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

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

python/ndg.security.server/ndg/security/server/AttAuthority/server-config.tac:

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_types.py,
python/ndg.security.common/ndg/security/common/AttAuthority/AttAuthority_services.py:
Include request denied message in getAttCertResponse.

python/ndg.security.server/ndg/security/server/AttAuthority/init.py:
fix to AttAuthorityAccessDenied? doc message.

python/ndg.security.server/ndg/security/server/SessionMgr/server-config.tac:
Exlpicitly convert AttCert? in response to string type.

python/ndg.security.server/ndg/security/server/SessionMgr/init.py:

  • make explicit imports from ndg.security.common.CredWallet?
  • make X509CertParse import
  • updated exception handling for getAttCert call to CredWallet?.

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/ndg.security.common/ndg/security/common/SessionMgr/SessionMgr_services_types.py:
Remove statusCode from getAttCertResponse - not needed.

python/ndg.security.test/ndg/security/test/AttAuthority/AttAuthorityClientTest.py:
minor updates to getAttCert tests.

python/ndg.security.test/ndg/security/test/MyProxy/myProxyClientTest.cfg:
fix to test1Store settings

python/ndg.security.test/ndg/security/test/MyProxy/Makefile:
makefile copies proxy obtained from MyProxy? ready for use in AttAuthority? client tests.

python/ndg.security.test/ndg/security/test/SessionMgr/SessionMgrClientTest.py:

  • add AttributeRequestDenied? import from SessionMgr?.
  • fix test4CookieDisconnect signing PKI settings
  • revised output tuple for getAttCert calls.
  • Added test6aCookieGetAttCertRefused to demonstrate attribute request denied exception
  • test3ProxyCertConnect signature verification failing at server!

python/ndg.security.test/ndg/security/test/SessionMgr/sessionMgrClientTest.cfg:
added more getAttCert test params.

python/ndg.security.common/ndg/security/common/AttAuthority/init.py:

python/ndg.security.common/ndg/security/common/wsSecurity.py:
comment out all print statements - only 'print decryptedData' affected in decrypt method
of EncryptionHandler?. This is not in use.

python/ndg.security.common/ndg/security/common/SessionMgr/init.py:

  • Added AttributeRequestDenied? exception for handling getAttCert calls.
  • msg now included in output tuple for getAttCert call.

python/ndg.security.common/ndg/security/common/AttCert.py:
Override XMLSecDoc parent class toString and str calls so that output is returned even
if the signature DOM object has not been initialised.

python/ndg.security.common/ndg/security/common/CredWallet.py:

  • 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        signedInfoC14nKw = {}
356        signedInfoC14nKw['unsuppressedPrefixes'] = ['xsi', 'xsd', 'SOAP-ENV', 'ds', 'wsse']
357        c14nMethodElem = signedInfoElem.createAppendElement(DSIG.BASE,
358                                                    'CanonicalizationMethod')
359        c14nMethodElem.node.setAttribute('Algorithm', DSIG.C14N_EXCL)
360        c14nInclNamespacesElem = c14nMethodElem.createAppendElement(\
361                                                    DSIG.C14N_EXCL,
362                                                    'InclusiveNamespaces')
363        c14nInclNamespacesElem.node.setAttribute('PrefixList', 
364                        ' '.join(signedInfoC14nKw['unsuppressedPrefixes']))
365       
366        # Signed Info - Signature method
367        sigMethodElem = signedInfoElem.createAppendElement(DSIG.BASE,
368                                                    'SignatureMethod')
369        #sigMethodElem.node.setAttribute('Algorithm', DSIG.DIGEST_SHA1)
370        sigMethodElem.node.setAttribute('Algorithm', DSIG.SIG_RSA_SHA1)
371       
372        # Signature - Signature value
373        signatureValueElem = signatureElem.createAppendElement(DSIG.BASE, 
374                                                             'SignatureValue')
375       
376        # Key Info
377        KeyInfoElem = signatureElem.createAppendElement(DSIG.BASE, 'KeyInfo')
378        secTokRefElem = KeyInfoElem.createAppendElement(OASIS.WSSE, 
379                                                  'SecurityTokenReference')
380       
381        # Reference back to the binary token included earlier
382        wsseRefElem = secTokRefElem.createAppendElement(OASIS.WSSE, 
383                                                        'Reference')
384        wsseRefElem.node.setAttribute('URI', "#binaryToken")
385       
386        # Add Reference to body so that it can be included in the signature
387        soapWriter.body.node.setAttribute('wsu:Id', "body")
388        soapWriter.body.node.setAttribute('xmlns:wsu', _WSU.UTILITY)
389
390        # Serialize and re-parse prior to reference generation - calculating
391        # canonicalization based on soapWriter.dom.node seems to give an
392        # error: the order of wsu:Id attribute is not correct
393        docNode = Reader().fromString(str(soapWriter))
394       
395        # Namespaces for XPath searches
396        processorNss = \
397        {
398            'ds':     DSIG.BASE, 
399            'wsu':    _WSU.UTILITY, 
400            'wsse':   OASIS.WSSE, 
401            'soapenv':"http://schemas.xmlsoap.org/soap/envelope/" 
402        }
403        ctxt = Context(docNode, processorNss=processorNss)
404        idNodes = xpath.Evaluate('//*[@wsu:Id]', 
405                                 contextNode=docNode, 
406                                 context=ctxt)
407       
408        # Leave out token
409        #idNodes = [idNodes[1]]
410
411        # 1) Reference Generation
412        #
413        # Find references
414        c14nKw = {}
415        c14nKw['unsuppressedPrefixes'] = ['xmlns', 'xsi', 'xsd', 'SOAP-ENV', 'wsu', 'wsse', 'ns1']
416        for idNode in idNodes:
417           
418            # Set URI attribute to point to reference to be signed
419            #uri = u"#" + idNode.getAttribute('wsu:Id')
420            uri = u"#" + idNode.attributes[(_WSU.UTILITY, 'Id')].value
421           
422            # Canonicalize reference
423            c14nRef = Canonicalize(idNode, **c14nKw)
424           
425            # Calculate digest for reference and base 64 encode
426            #
427            # Nb. encodestring adds a trailing newline char
428            digestValue = base64.encodestring(sha(c14nRef).digest()).strip()
429
430
431            # Add a new reference element to SignedInfo
432            refElem = signedInfoElem.createAppendElement(DSIG.BASE, 
433                                                         'Reference')
434            refElem.node.setAttribute('URI', uri)
435           
436            # Use ds:Transforms or wsse:TransformationParameters?
437            transformsElem = refElem.createAppendElement(DSIG.BASE, 
438                                                        'Transforms')
439            transformElem = transformsElem.createAppendElement(DSIG.BASE, 
440                                                               'Transform')
441#            transformElem.node.setAttribute('Algorithm', DSIG.C14N)
442            transformElem.node.setAttribute('Algorithm', DSIG.C14N_EXCL)
443
444            inclNamespacesElem = transformElem.createAppendElement(\
445                                                        DSIG.C14N_EXCL,
446                                                       'InclusiveNamespaces')
447            inclNamespacesElem.node.setAttribute('PrefixList',
448                                ' '.join(c14nKw['unsuppressedPrefixes']))
449           
450            # Digest Method
451            digestMethodElem = refElem.createAppendElement(DSIG.BASE, 
452                                                           'DigestMethod')
453            digestMethodElem.node.setAttribute('Algorithm', DSIG.DIGEST_SHA1)
454           
455            # Digest Value
456            digestValueElem = refElem.createAppendElement(DSIG.BASE, 
457                                                          'DigestValue')
458            digestValueElem.createAppendTextNode(digestValue)
459
460   
461        # 2) Signature Generation
462        #
463
464        # Test against signature generated by pyXMLSec version
465        #xmlTxt = open('./wsseSign-xmlsec-res.xml').read()
466        #dom = NonvalidatingReader.parseStream(StringIO(xmlTxt))
467       
468        # Canonicalize the signedInfo node
469        #
470        # Nb. When extracted the code adds the namespace attribute to the
471        # signedInfo!  This has important consequences for validation -
472        #
473        # 1) Do you strip the namespace attribute before taking the digest to
474        # ensure the text is exactly the same as what is displayed in the
475        # message?
476        #
477        # 2) Leave it in and assume the validation algorithm will expect to
478        # add in the namespace attribute?!
479        #
480        # http://www.w3.org/TR/xml-c14n#NoNSPrefixRewriting implies you need
481        # to include namespace declarations for namespaces referenced in a doc
482        # subset - yes to 2)
483        #c14nSignedInfo = signedInfoElem.canonicalize()
484        c14nSignedInfo = Canonicalize(signedInfoElem.node, **signedInfoC14nKw)
485
486        # Calculate digest of SignedInfo
487        signedInfoDigestValue = sha(c14nSignedInfo).digest().strip()
488       
489        # Sign using the private key and base 64 encode the result
490        signatureValue = self.__signingPriKey.sign(signedInfoDigestValue)
491        b64EncSignatureValue = base64.encodestring(signatureValue).strip()
492
493        # Add to <SignatureValue>
494        signatureValueElem.createAppendTextNode(b64EncSignatureValue)
495       
496#        # Extract RSA public key from the cert
497#        rsaPubKey = self.__signingCert.m2CryptoX509.get_pubkey().get_rsa()
498       
499        # Check the signature
500#        verify = bool(rsaPubKey.verify(signedInfoDigestValue, signatureValue))
501#       
502#        open('soap.xml', 'w').write(str(soapWriter))
503#        import pdb;pdb.set_trace()
504#        print "Signature Generated"
505#        print str(soapWriter)
506
507
508    def verify(self, parsedSOAP):
509        """Verify signature"""
510
511        processorNss = \
512        {
513            'ds':     DSIG.BASE, 
514            'wsu':    _WSU.UTILITY, 
515            'wsse':   OASIS.WSSE, 
516            'soapenv':"http://schemas.xmlsoap.org/soap/envelope/" 
517        }
518        ctxt = Context(parsedSOAP.dom, processorNss=processorNss)
519       
520
521        signatureNodes = xpath.Evaluate('//ds:Signature', 
522                                        contextNode=parsedSOAP.dom, 
523                                        context=ctxt)
524        if len(signatureNodes) > 1:
525            raise VerifyError, 'Multiple ds:Signature elements found'
526       
527        try:
528            signatureNodes = signatureNodes[0]
529        except:
530            # Message wasn't signed
531            return
532       
533        # Two stage process: reference validation followed by signature
534        # validation
535       
536        # 1) Reference Validation
537       
538        # Check for canonicalization set via ds:CanonicalizationMethod -
539        # Use this later as a back up in case no Canonicalization was set in
540        # the transforms elements
541        c14nMethodNode = xpath.Evaluate('//ds:CanonicalizationMethod', 
542                                        contextNode=parsedSOAP.dom, 
543                                        context=ctxt)[0]
544       
545        refNodes = xpath.Evaluate('//ds:Reference', 
546                                  contextNode=parsedSOAP.dom, 
547                                  context=ctxt)
548           
549        for refNode in refNodes:
550            # Get the URI for the reference
551            refURI = refNode.getAttributeNodeNS(None, 'URI').value
552                           
553            try:
554                transformsNode = getElements(refNode, "Transforms")[0]
555                transforms = getElements(transformsNode, "Transform")
556   
557                refAlgorithm = transforms[0].getAttributeNodeNS(None, 
558                                                         "Algorithm").value
559            except Exception, e:
560                raise VerifyError, \
561            'failed to get transform algorithm for <ds:Reference URI="%s">'%\
562                        (refURI, str(e))
563               
564            # Add extra keyword for Exclusive canonicalization method
565            c14nKw = {}
566            if refAlgorithm == DSIG.C14N_EXCL:
567                try:
568                    inclusiveNS = getElements(transforms[0], 
569                                              "InclusiveNamespaces")
570                   
571                    pfxListAttNode = inclusiveNS[0].getAttributeNodeNS(None, 
572                                                               'PrefixList')
573                    c14nKw['unsuppressedPrefixes'] = \
574                                                pfxListAttNode.value.split()
575                except:
576                    raise VerifyError, \
577                'failed to handle transform (%s) in <ds:Reference URI="%s">'%\
578                        (transforms[0], refURI)
579       
580            # Canonicalize the reference data and calculate the digest
581            if refURI[0] != "#":
582                raise VerifyError, \
583                    "Expecting # identifier for Reference URI \"%s\"" % refURI
584                   
585            # XPath reference
586            uriXPath = '//*[@wsu:Id="%s"]' % refURI[1:]
587            uriNode = xpath.Evaluate(uriXPath, 
588                                     contextNode=parsedSOAP.dom, 
589                                     context=ctxt)[0]
590
591            c14nRef = Canonicalize(uriNode, **c14nKw)
592            digestValue = base64.encodestring(sha(c14nRef).digest()).strip()
593           
594            # Extract the digest value that was stored           
595            digestNode = getElements(refNode, "DigestValue")[0]
596            nodeDigestValue = str(digestNode.childNodes[0].nodeValue).strip()   
597           
598            # Reference validates if the two digest values are the same
599            if digestValue != nodeDigestValue:
600                raise InvalidSignature, \
601                        'Digest Values do not match for URI: "%s"' % refURI
602               
603        # 2) Signature Validation
604        signedInfoNode = xpath.Evaluate('//ds:SignedInfo',
605                                        contextNode=parsedSOAP.dom, 
606                                        context=ctxt)[0]
607
608        #import pdb;pdb.set_trace()
609        # Get algorithm used for canonicalization of the SignedInfo
610        # element.  Nb. This is NOT necessarily the same as that used to
611        # canonicalize the reference elements checked above!
612        signedInfoC14nAlg = c14nMethodNode.getAttributeNodeNS(None, 
613                                                         "Algorithm").value
614        signedInfoC14nKw = {}
615        if signedInfoC14nAlg == DSIG.C14N_EXCL:
616            try:
617                inclusiveNS = getElements(c14nMethodNode,
618                                          "InclusiveNamespaces")
619               
620                pfxListAttNode = inclusiveNS[0].getAttributeNodeNS(None, 
621                                                           'PrefixList')
622                signedInfoC14nKw['unsuppressedPrefixes'] = \
623                                            pfxListAttNode.value.split()
624            except Exception, e:
625                raise VerifyError, \
626            'failed to handle exclusive canonicalisation for SignedInfo: %s'%\
627                        str(e)
628
629        # Canonicalize the SignedInfo node and take digest
630        c14nSignedInfo = Canonicalize(signedInfoNode, **signedInfoC14nKw)       
631        signedInfoDigestValue = sha(c14nSignedInfo).digest()
632       
633        # Get the signature value in order to check against the digest just
634        # calculated
635        signatureValueNode = xpath.Evaluate('//ds:SignatureValue',
636                                            contextNode=parsedSOAP.dom, 
637                                            context=ctxt)[0]
638
639        # Remove base 64 encoding
640        b64EncSignatureValue = \
641                    str(signatureValueNode.childNodes[0].nodeValue).strip()
642                   
643        signatureValue = base64.decodestring(b64EncSignatureValue)
644
645
646        # Look for X.509 Cert in wsse:BinarySecurityToken node
647        try:
648            binSecTokNode = xpath.Evaluate('//wsse:BinarySecurityToken',
649                                           contextNode=parsedSOAP.dom,
650                                           context=ctxt)[0]
651        except:
652            # Signature may not have included the Binary Security Token in
653            # which case the verifying cert will need to have been set
654            # elsewhere
655            pass 
656       
657       
658        try:
659            b64EncX509Cert = self.__class__.__beginCert + \
660                         str(binSecTokNode.childNodes[0]._get_nodeValue()) + \
661                         self.__class__.__endCert
662                         
663            self.__setVerifyingCert(b64EncX509Cert)
664        except Exception, e:
665            raise VerifyError, "Error extracting BinarySecurityToken from " +\
666                                "WSSE header: " + str(e)
667       
668        if self.__verifyingCert is None:
669            raise VerifyError, "No certificate set for verification " + \
670                "of the signature"
671       
672       
673        # Extract RSA public key from the cert
674        rsaPubKey = self.__verifyingCert.m2CryptoX509.get_pubkey().get_rsa()
675       
676        # Apply the signature verification
677        try:
678            verify = bool(rsaPubKey.verify(signedInfoDigestValue, 
679                                           signatureValue))
680        except RSA.RSAError, e:
681            raise VerifyError, "Error in Signature: " + str(e)
682       
683        if not verify:
684            raise InvalidSignature, "Invalid signature"
685       
686        #print "Signature OK"
687
688
689class EncryptionError(Exception):
690    """Flags an error in the encryption process"""
691
692class DecryptionError(Exception):
693    """Raised from EncryptionHandler.decrypt if an error occurs with the
694    decryption process"""
695
696
697class EncryptionHandler(object):
698    """Encrypt/Decrypt SOAP messages using WS-Security""" 
699   
700    # Map namespace URIs to Crypto algorithm module and mode
701    cryptoAlg = \
702    {
703         _ENCRYPTION.WRAP_AES256:      {'module':       AES, 
704                                        'mode':         AES.MODE_ECB,
705                                        'blockSize':    16},
706         
707         # CBC (Cipher Block Chaining) modes
708         _ENCRYPTION.BLOCK_AES256:     {'module':       AES, 
709                                        'mode':         AES.MODE_CBC,
710                                        'blockSize':    16},
711                                       
712         _ENCRYPTION.BLOCK_TRIPLEDES:  {'module':       DES3, 
713                                        'mode':         DES3.MODE_CBC,
714                                        'blockSize':    8}   
715    }
716
717     
718    def __init__(self,
719                 signingCertFilePath=None, 
720                 signingPriKeyFilePath=None, 
721                 signingPriKeyPwd=None,
722                 chkSecurityTokRef=False,
723                 encrNS=_ENCRYPTION.BLOCK_AES256):
724       
725        self.__signingCertFilePath = signingCertFilePath
726        self.__signingPriKeyFilePath = signingPriKeyFilePath
727        self.__signingPriKeyPwd = signingPriKeyPwd
728       
729        self.__chkSecurityTokRef = chkSecurityTokRef
730       
731        # Algorithm for shared key encryption
732        try:
733            self.__encrAlg = self.cryptoAlg[encrNS]
734           
735        except KeyError:
736            raise EncryptionError, \
737        'Input encryption algorithm namespace "%s" is not supported' % encrNS
738
739        self.__encrNS = encrNS
740       
741       
742    def encrypt(self, soapWriter):
743        """Encrypt an outbound SOAP message
744       
745        Use Key Wrapping - message is encrypted using a shared key which
746        itself is encrypted with the public key provided by the X.509 cert.
747        signingCertFilePath"""
748       
749        # Use X.509 Cert to encrypt
750        x509Cert = X509.load_cert(self.__signingCertFilePath)
751       
752        soapWriter.dom.setNamespaceAttribute('wsse', OASIS.WSSE)
753        soapWriter.dom.setNamespaceAttribute('xenc', _ENCRYPTION.BASE)
754        soapWriter.dom.setNamespaceAttribute('ds', DSIG.BASE)
755       
756        # TODO: Put in a check to make sure <wsse:security> isn't already
757        # present in header
758        wsseElem = soapWriter._header.createAppendElement(OASIS.WSSE, 
759                                                         'Security')
760        wsseElem.node.setAttribute('SOAP-ENV:mustUnderstand', "1")
761       
762        encrKeyElem = wsseElem.createAppendElement(_ENCRYPTION.BASE, 
763                                                   'EncryptedKey')
764       
765        # Encryption method used to encrypt the shared key
766        keyEncrMethodElem = encrKeyElem.createAppendElement(_ENCRYPTION.BASE, 
767                                                        'EncryptionMethod')
768       
769        keyEncrMethodElem.node.setAttribute('Algorithm', 
770                                            _ENCRYPTION.KT_RSA_1_5)
771
772
773        # Key Info
774        KeyInfoElem = encrKeyElem.createAppendElement(DSIG.BASE, 'KeyInfo')
775       
776        secTokRefElem = KeyInfoElem.createAppendElement(OASIS.WSSE, 
777                                                  'SecurityTokenReference')
778       
779        x509IssSerialElem = secTokRefElem.createAppendElement(DSIG.BASE, 
780                                                          'X509IssuerSerial')
781
782       
783        x509IssNameElem = x509IssSerialElem.createAppendElement(DSIG.BASE, 
784                                                          'X509IssuerName')
785        x509IssNameElem.createAppendTextNode(x509Cert.get_issuer().as_text())
786
787       
788        x509IssSerialNumElem = x509IssSerialElem.createAppendElement(
789                                                  DSIG.BASE, 
790                                                  'X509IssuerSerialNumber')
791       
792        x509IssSerialNumElem.createAppendTextNode(
793                                          str(x509Cert.get_serial_number()))
794
795        # References to what has been encrypted
796        encrKeyCiphDataElem = encrKeyElem.createAppendElement(
797                                                          _ENCRYPTION.BASE,
798                                                          'CipherData')
799       
800        encrKeyCiphValElem = encrKeyCiphDataElem.createAppendElement(
801                                                          _ENCRYPTION.BASE,
802                                                          'CipherValue')
803
804        # References to what has been encrypted
805        refListElem = encrKeyElem.createAppendElement(_ENCRYPTION.BASE,
806                                                      'ReferenceList')
807       
808        dataRefElem = refListElem.createAppendElement(_ENCRYPTION.BASE,
809                                                      'DataReference')
810        dataRefElem.node.setAttribute('URI', "#encrypted")
811
812                     
813        # Add Encrypted data to SOAP body
814        encrDataElem = soapWriter.body.createAppendElement(_ENCRYPTION.BASE, 
815                                                           'EncryptedData')
816        encrDataElem.node.setAttribute('Id', 'encrypted')
817        encrDataElem.node.setAttribute('Type', _ENCRYPTION.BASE) 
818             
819        # Encryption method used to encrypt the target data
820        dataEncrMethodElem = encrDataElem.createAppendElement(
821                                                      _ENCRYPTION.BASE, 
822                                                      'EncryptionMethod')
823       
824        dataEncrMethodElem.node.setAttribute('Algorithm', self.__encrNS)
825       
826        # Cipher data
827        ciphDataElem = encrDataElem.createAppendElement(_ENCRYPTION.BASE,
828                                                        'CipherData')
829       
830        ciphValueElem = ciphDataElem.createAppendElement(_ENCRYPTION.BASE,
831                                                         'CipherValue')
832
833
834        # Get elements from SOAP body for encryption
835        dataElem = soapWriter.body.node.childNodes[0]
836        data = dataElem.toxml()
837     
838        # Pad data to nearest multiple of encryption algorithm's block size   
839        modData = len(data) % self.__encrAlg['blockSize']
840        nPad = modData and self.__encrAlg['blockSize'] - modData or 0
841       
842        # PAd with random junk but ...
843        data += os.urandom(nPad-1)
844       
845        # Last byte should be number of padding bytes
846        # (http://www.w3.org/TR/xmlenc-core/#sec-Alg-Block)
847        data += chr(nPad)       
848       
849        # Generate shared key and input vector - for testing use hard-coded
850        # values to allow later comparison             
851        sharedKey = os.urandom(self.__encrAlg['blockSize'])
852        iv = os.urandom(self.__encrAlg['blockSize'])
853       
854        alg = self.__encrAlg['module'].new(sharedKey,
855                                           self.__encrAlg['mode'],
856                                           iv)
857 
858        # Encrypt required elements - prepend input vector
859        encryptedData = alg.encrypt(iv + data)
860        dataCiphValue = base64.encodestring(encryptedData).strip()
861
862        ciphValueElem.createAppendTextNode(dataCiphValue)
863       
864       
865        # ! Delete unencrypted message body elements !
866        soapWriter.body.node.removeChild(dataElem)
867
868       
869        # Use X.509 cert public key to encrypt the shared key - Extract key
870        # from the cert
871        rsaPubKey = x509Cert.get_pubkey().get_rsa()
872       
873        # Encrypt the shared key
874        encryptedSharedKey = rsaPubKey.public_encrypt(sharedKey, 
875                                                      RSA.pkcs1_padding)
876       
877        encrKeyCiphVal = base64.encodestring(encryptedSharedKey).strip()
878       
879        # Add the encrypted shared key to the EncryptedKey section in the SOAP
880        # header
881        encrKeyCiphValElem.createAppendTextNode(encrKeyCiphVal)
882
883#        print soapWriter.dom.node.toprettyxml()
884#        import pdb;pdb.set_trace()
885       
886       
887    def decrypt(self, parsedSOAP):
888        """Decrypt an inbound SOAP message"""
889       
890        processorNss = \
891        {
892            'xenc':   _ENCRYPTION.BASE,
893            'ds':     DSIG.BASE, 
894            'wsu':    _WSU.UTILITY, 
895            'wsse':   OASIS.WSSE, 
896            'soapenv':"http://schemas.xmlsoap.org/soap/envelope/" 
897        }
898        ctxt = Context(parsedSOAP.dom, processorNss=processorNss)
899       
900        refListNodes = xpath.Evaluate('//xenc:ReferenceList', 
901                                      contextNode=parsedSOAP.dom, 
902                                      context=ctxt)
903        if len(refListNodes) > 1:
904            raise DecryptionError, 'Expecting a single ReferenceList element'
905       
906        try:
907            refListNode = refListNodes[0]
908        except:
909            # Message wasn't encrypted - is this OK or is a check needed for
910            # encryption info in SOAP body - enveloped form?
911            return
912
913
914        # Check for wrapped key encryption
915        encrKeyNodes = xpath.Evaluate('//xenc:EncryptedKey', 
916                                      contextNode=parsedSOAP.dom, 
917                                      context=ctxt)
918        if len(encrKeyNodes) > 1:
919            raise DecryptionError, 'This implementation can only handle ' + \
920                                   'single EncryptedKey element'
921       
922        try:
923            encrKeyNode = encrKeyNodes[0]
924        except:
925            # Shared key encryption used - leave out for the moment
926            raise DecryptionError, 'This implementation can only handle ' + \
927                                   'wrapped key encryption'
928
929       
930        # Check encryption method
931        keyEncrMethodNode = getElements(encrKeyNode, 'EncryptionMethod')[0]     
932        keyAlgorithm = keyEncrMethodNode.getAttributeNodeNS(None, 
933                                                            "Algorithm").value
934        if keyAlgorithm != _ENCRYPTION.KT_RSA_1_5:
935            raise DecryptionError, \
936            'Encryption algorithm for wrapped key is "%s", expecting "%s"' % \
937                (keyAlgorithm, _ENCRYPTION.KT_RSA_1_5)
938
939                                                           
940        if self.__chkSecurityTokRef and self.__signingCertFilePath:
941             
942            # Check input cert. against SecurityTokenReference
943            securityTokRefXPath = '/ds:KeyInfo/wsse:SecurityTokenReference'
944            securityTokRefNode = xpath.Evaluate(securityTokRefXPath, 
945                                                contextNode=encrKeyNode, 
946                                                context=ctxt)
947            # TODO: Look for ds:X509* elements to check against X.509 cert
948            # input
949
950
951        # Look for cipher data for wrapped key
952        keyCiphDataNode = getElements(encrKeyNode, 'CipherData')[0]
953        keyCiphValNode = getElements(keyCiphDataNode, 'CipherValue')[0]
954
955        keyCiphVal = str(keyCiphValNode.childNodes[0].nodeValue)
956        encryptedKey = base64.decodestring(keyCiphVal)
957
958        # Read RSA Private key in order to decrypt wrapped key 
959        priKeyFile = BIO.File(open(self.__signingPriKeyFilePath))         
960        pwdCallback = lambda *ar, **kw: self.__signingPriKeyPwd                                       
961        priKey = RSA.load_key_bio(priKeyFile, callback=pwdCallback)
962       
963        sharedKey = priKey.private_decrypt(encryptedKey, RSA.pkcs1_padding)
964       
965
966        # Check list of data elements that have been encrypted
967        for dataRefNode in refListNode.childNodes:
968
969            # Get the URI for the reference
970            dataRefURI = dataRefNode.getAttributeNodeNS(None, 'URI').value                           
971            if dataRefURI[0] != "#":
972                raise VerifyError, \
973                    "Expecting # identifier for DataReference URI \"%s\"" % \
974                    dataRefURI
975
976            # XPath reference - need to check for wsu namespace qualified?
977            #encrNodeXPath = '//*[@wsu:Id="%s"]' % dataRefURI[1:]
978            encrNodeXPath = '//*[@Id="%s"]' % dataRefURI[1:]
979            encrNode = xpath.Evaluate(encrNodeXPath, 
980                                      contextNode=parsedSOAP.dom, 
981                                      context=ctxt)[0]
982               
983            dataEncrMethodNode = getElements(encrNode, 'EncryptionMethod')[0]     
984            dataAlgorithm = dataEncrMethodNode.getAttributeNodeNS(None, 
985                                                            "Algorithm").value
986            try:       
987                # Match algorithm name to Crypto module
988                CryptoAlg = self.cryptoAlg[dataAlgorithm]
989               
990            except KeyError:
991                raise DecryptionError, \
992'Encryption algorithm for data is "%s", supported algorithms are:\n "%s"' % \
993                    (keyAlgorithm, "\n".join(self.cryptoAlg.keys()))
994
995            # Get Data
996            dataCiphDataNode = getElements(encrNode, 'CipherData')[0]
997            dataCiphValNode = getElements(dataCiphDataNode, 'CipherValue')[0]
998       
999            dataCiphVal = str(dataCiphValNode.childNodes[0].nodeValue)
1000            encryptedData = base64.decodestring(dataCiphVal)
1001           
1002            alg = CryptoAlg['module'].new(sharedKey, CryptoAlg['mode'])
1003            decryptedData = alg.decrypt(encryptedData)
1004           
1005            # Strip prefix - assume is block size
1006            decryptedData = decryptedData[CryptoAlg['blockSize']:]
1007           
1008            # Strip any padding suffix - Last byte should be number of padding
1009            # bytes
1010            # (http://www.w3.org/TR/xmlenc-core/#sec-Alg-Block)
1011            lastChar = decryptedData[-1]
1012            nPad = ord(lastChar)
1013           
1014            # Sanity check - there may be no padding at all - the last byte
1015            # being the end of the encrypted XML?
1016            #
1017            # TODO: are there better sanity checks than this?!
1018            if nPad < CryptoAlg['blockSize'] and nPad > 0 and \
1019               lastChar != '\n' and lastChar != '>':
1020               
1021                # Follow http://www.w3.org/TR/xmlenc-core/#sec-Alg-Block -
1022                # last byte gives number of padding bytes
1023                decryptedData = decryptedData[:-nPad]
1024
1025
1026            # Parse the encrypted data - inherit from Reader as a fudge to
1027            # enable relevant namespaces to be added prior to parse
1028            processorNss.update({'xsi': SCHEMA.XSI3, 'ns1': 'urn:ZSI:examples'})
1029            class _Reader(Reader):
1030                def initState(self, ownerDoc=None):
1031                    Reader.initState(self, ownerDoc=ownerDoc)
1032                    self._namespaces.update(processorNss)
1033                   
1034            rdr = _Reader()
1035            dataNode = rdr.fromString(decryptedData, ownerDoc=parsedSOAP.dom)
1036           
1037            # Add decrypted element to parent and remove encrypted one
1038            parentNode = encrNode._get_parentNode()
1039            parentNode.appendChild(dataNode)
1040            parentNode.removeChild(encrNode)
1041           
1042            from xml.dom.ext import ReleaseNode
1043            ReleaseNode(encrNode)
1044           
1045            # Ensure body_root attribute is up to date in case it was
1046            # previously encrypted
1047            parsedSOAP.body_root = parsedSOAP.body.childNodes[0]
1048            #print decryptedData
1049            #import pdb;pdb.set_trace()
1050
1051
1052#_____________________________________________________________________________
1053from zope.interface import classProvides, implements, Interface
1054import twisted.web.http
1055from twisted.python import log, failure
1056
1057from ZSI.twisted.WSresource import DefaultHandlerChain, \
1058    DefaultCallbackHandler, CallbackChainInterface, HandlerChainInterface, \
1059    DataHandler
1060   
1061from ZSI import _get_element_nsuri_name, EvaluateException, ParseException
1062   
1063   
1064class WSSecurityHandlerChainFactory:
1065    protocol = DefaultHandlerChain
1066   
1067    @classmethod
1068    def newInstance(cls):
1069        return cls.protocol(DefaultCallbackHandler, 
1070                            DataHandler,
1071                            WSSecurityHandler)
1072   
1073
1074class WSSecurityHandler:
1075    classProvides(HandlerChainInterface)
1076
1077    signatureHandler = None
1078   
1079    @classmethod
1080    def processRequest(cls, ps, **kw):
1081        """invokes callback that should return a (request,response) tuple.
1082        representing the SOAP request and response respectively.
1083        ps -- ParsedSoap instance representing HTTP Body.
1084        request -- twisted.web.server.Request
1085        """
1086        if cls.signatureHandler:
1087            cls.signatureHandler.verify(ps)
1088           
1089        return ps
1090   
1091    @classmethod
1092    def processResponse(cls, sw, **kw):
1093       
1094        if cls.signatureHandler:
1095            cls.signatureHandler.sign(sw)
1096           
1097        return sw
Note: See TracBrowser for help on using the repository browser.