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

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

Replaced socket.ssl with M2Crypto https for web service clients

ndg.security.test/ndg/security/test/AttAuthority/siteAAttAuthorityProperties.xml:
swap to https

ndg.security.test/ndg/security/test/AttAuthority/AttAuthorityClientTest.py:

  • added X509CertRead import for sslCACertList keyword processing
  • added sslPeerCertCN keyword input to AA client

ndg.security.test/ndg/security/test/AttAuthority/attAuthorityClientTest.cfg:

  • new keywords for SSL connections: sslpeercertcn and sslcacertfilepathlist

ndg.security.test/ndg/security/test/MyProxy/Makefile: PYTHONPATH macro to
enable custom python path setting

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

  • added sslPeerCertCN and sslCACertList keyword input SM client - M2Crypto

SSL integration not complete!!

ndg.security.common/ndg/security/common/ca/init.py:

  • fix to include HTTPResponse from ZSI.wstools.Utility

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

  • urlparse import - use to determine http/https transport
  • new ndg.security.common.m2CryptoSSLUtility module used for M2Crypto SSL
  • added sslPeerCertCN property
  • removed getSrvX509Cert() - no longer needed
  • modified call to Binding in initService to use custom M2Crypto SSL client
  • Removed exception handling for soap call wrappers - these can surpress

useful info from being reported back higher in the stack

ndg.security.common/ndg/security/common/X509.py:

  • bug fix to X509Cert.init - init caX509Stack to []
  • altered X509Stack.verifyCertChain to enable verification be self stack

rather than need for caX509Stack

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

  • urlparse import - use to determine http/https transport
  • new ndg.security.common.m2CryptoSSLUtility module used for M2Crypto SSL
  • added sslPeerCertCN and sslCACertList properties for SSL host checks
  • removed setSrvCertFilePath() and getSrvCert() - no longer needed
  • modified call to Binding in initService to use custom M2Crypto SSL client
  • Removed exception handling for soap call wrappers - these can surpress

useful info from being reported back higher in the stack

ndg.security.common/ndg/security/common/wsSecurity.py:

  • bug fix to binSecTokValType class var - 'X509' wrongly keyed into 'X509v3'

namespace

ndg.security.common/ndg/security/common/m2CryptoSSLUtility.py: new module
containing class to extend M2Crypto.httpslib.HTTPSConnection and
M2Crypto.SSL.Checker.Checker

  • 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
7@author P J Kershaw 01/09/06
8
9@copyright (C) 2006 CCLRC & NERC
10
11@license This software may be distributed under the terms of the Q Public
12License, version 1.0 or later.
13"""
14
15__revision__ = '$Id:$'
16
17import re
18
19# Digest and signature/verify
20from sha import sha
21from M2Crypto import X509, BIO, RSA
22import base64
23
24# Conditional import as this is required for the encryption
25# handler
26try:
27    # For shared key encryption
28    from Crypto.Cipher import AES, DES3
29except:
30    pass
31
32import os
33
34import ZSI
35from ZSI.wstools.Namespaces import DSIG, ENCRYPTION, OASIS, WSU, WSA200403, \
36                                   SOAP, SCHEMA # last included for xsi
37                                   
38from ZSI.TC import ElementDeclaration,TypeDefinition
39from ZSI.generate.pyclass import pyclass_type
40
41from ZSI.wstools.Utility import DOMException
42from ZSI.wstools.Utility import NamespaceError, MessageInterface, ElementProxy
43
44# Canonicalization
45from ZSI.wstools.c14n import Canonicalize
46
47from xml.dom import Node
48from xml.xpath.Context import Context
49from xml import xpath
50
51# Include for re-parsing doc ready for canonicalization in sign method - see
52# associated note
53from xml.dom.ext.reader.PyExpat import Reader
54
55
56from ndg.security.common.X509 import X509Cert, X509CertParse, X509CertRead, \
57X509Stack, X509StackParseFromDER
58
59
60class _ENCRYPTION(ENCRYPTION):
61    '''Derived from ENCRYPTION class to add in extra 'tripledes-cbc' - is this
62    any different to 'des-cbc'?  ENCRYPTION class implies that it is the same
63    because it's assigned to 'BLOCK_3DES' ??'''
64    BLOCK_TRIPLEDES = "http://www.w3.org/2001/04/xmlenc#tripledes-cbc"
65
66class _WSU(WSU):
67    '''Try different utility namespace for use with WebSphere'''
68    #UTILITY = "http://schemas.xmlsoap.org/ws/2003/06/utility"
69    UTILITY = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"
70   
71def getElements(node, nameList):
72    '''DOM Helper function for getting child elements from a given node'''
73    # Avoid sub-string matches
74    nameList = isinstance(nameList, basestring) and [nameList] or nameList
75    return [n for n in node.childNodes if str(n.localName) in nameList]
76
77
78class WSSecurityError(Exception):
79    """For WS-Security generic exceptions not covered by other exception
80    classes in this module"""
81
82class InvalidCertChain(Exception):   
83    """Raised from SignatureHandler.verify if the certificate submitted to
84    verify a signature is not from a known CA"""
85   
86class VerifyError(Exception):
87    """Raised from SignatureHandler.verify if an error occurs in the signature
88    verification"""
89   
90class InvalidSignature(Exception):
91    """Raised from verify method for an invalid signature"""
92
93class SignatureError(Exception):
94    """Flag if an error occurs during signature generation"""
95       
96class SignatureHandler(object):
97    """Class to handle signature and verification of signature with
98    WS-Security
99   
100    @type __beginCert: string
101    @param __beginCert: delimiter for beginning of base64 encoded portion of
102    a PEM encoded X.509 certificate
103    @type __endCert: string
104    @cvar: __endCert: equivalent end delimiter
105   
106    @type __x509CertPat: regular expression pattern object
107    @cvar __x509CertPat: regular expression for extracting the base64 encoded
108    portion of a PEM encoded X.509 certificate
109    @cvar binSecTokValType: supported ValueTypes for BinarySecurityToken
110    element in WSSE header
111    @type binSecTokValType: dict"""
112   
113    __beginCert = '-----BEGIN CERTIFICATE-----\n'
114    __endCert = '\n-----END CERTIFICATE-----'
115    __x509CertPat = re.compile(__beginCert + \
116                               '?(.*?)\n?-----END CERTIFICATE-----',
117                               re.S)
118   
119    binSecTokValType = {
120        "X509PKIPathv1": "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509PKIPathv1",
121        "X509":          "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509",
122        "X509v3":        "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509v3"
123    }
124   
125    #_________________________________________________________________________
126    def __init__(self,
127                 reqBinSecTokValType="X509v3",
128                 verifyingCert=None,
129                 verifyingCertFilePath=None,
130                 signingCert=None,
131                 signingCertFilePath=None, 
132                 signingCertChain=None,
133                 signingPriKey=None,
134                 signingPriKeyFilePath=None, 
135                 signingPriKeyPwd=None,
136                 caCertDirPath=None,
137                 caCertFilePathList=[],
138                 refC14nKw={'unsuppressedPrefixes': ['xmlns', 
139                                                     'xsi', 
140                                                     'xsd', 
141                                                     'SOAP-ENV', 
142                                                     'wsu', 
143                                                     'wsse', 
144                                                     'ns1']},
145                # Added 'ec' to list P J Kershaw 01/02/07
146                signedInfoC14nKw = {'unsuppressedPrefixes': ['xsi', 
147                                                             'xsd', 
148                                                             'SOAP-ENV', 
149                                                             'ds', 
150                                                             'wsse', 
151                                                             'ec']}):
152        '''
153        @reqBinSecTokValType: set the ValueType for the BinarySecurityToken
154        added to the WSSE header for a signed message.  See
155        __setReqBinSecTokValType method and binSecTokValType class variable
156        for options.  binSecTokValType determines whether signingCert or
157        signingCertChain attributes will be used.       
158        @type binSecTokValType: string
159       
160        @keyword verifyingCert: X.509 certificate used by verify method to
161        verify a message.  This argument can be omitted if the message to
162        be verified contains the X.509 certificate in the
163        BinarySecurityToken element.  In this case, the cert read from the
164        message will be assigned to the verifyingCert attribute.
165        @type verifyingCert: M2Crypto.X509.X509 /
166        ndg.security.common.X509.X509Cert
167       
168        @keyword verifyingCertFilePath: alternative input to the above, pass
169        the file path to the certificate stored in a file
170        @type verifyingCertFilePath: string
171       
172        @keyword signingCert: certificate associated with private key used to
173        sign a message.  The sign method will add this to the
174        BinarySecurityToken element of the WSSE header.  binSecTokValType
175        attribute must be set to 'X509' or 'X509v3' ValueTyep.  As an
176        alternative, use signingCertChain - see below...
177        @type signingCert: M2Crypto.X509.X509 /
178        ndg.security.common.X509.X509Cert
179       
180        @keyword signingCertFilePath: alternative input to the above, pass
181        the file path to the certificate stored in a file
182        @type signingCertFilePath: string
183       
184        @keyword signingCertChain: pass a list of certificates constituting a
185        chain of trust from the certificate used to verifying the signature
186        backward to the CA cert.  The CA cert need not be included.  To use
187        this option, reqBinSecTokValType must be set to the 'X509PKIPathv1'
188        ValueType
189        @type signingCertChain: list or tuple
190       
191        @keyword signingPriKey: private key used to be sign method to sign
192        message
193        @type signingPriKey: M2Crypto.RSA.
194       
195        @keyword signingPriKeyFilePath: equivalent to the above but pass
196        private key from PEM file
197        @type signingPriKeyFilePath: string
198       
199        @keyword signingPriKeyPwd: password protecting private key.  Set /
200        default to None if there is no password.
201        @type signingPriKeyPwd: string or None
202       
203        @keyword caCertDirPath: establish trust for signature verification.
204        This is a directory containing CA certificates.  These are used to
205        verify the certificate used to verify the message signature.
206        @type caCertDirPath: string
207       
208        @keyword caCertFilePathList: same as above except pass in a list of
209        file paths instead of a single directory name.
210        @type caCertFilePathList: list or tuple
211       
212        @keyword refC14nKw: dictionary of keywords to reference
213        Canonicalization.  Use 'unsuppressedPrefixes' keyword to set
214        unsuppressedPrefixes.
215        @type refC14nKw: dict
216       
217        @keyword signedInfoC14nKw: keywords to Signed Info Canonicalization.
218        It uses the same format as refC14nKw above.
219        @type signedInfoC14nKw: dict
220        '''
221       
222        self.__setReqBinSecTokValType(reqBinSecTokValType)
223       
224        # Set keywords for canonicalization of SignedInfo and reference
225        # elements
226        self.__setRefC14nKw(refC14nKw)
227        self.__setSignedInfoC14nKw(signedInfoC14nKw)
228           
229
230        self.__setVerifyingCert(verifyingCert)
231        self.__setVerifyingCertFilePath(verifyingCertFilePath)
232       
233        self.__setSigningCert(signingCert)
234        self.__setSigningCertFilePath(signingCertFilePath)
235
236        if signingCertChain:
237            self.__setSigningCertChain(signingCertChain)
238        else:
239            self.__signingCertChain = None   
240             
241        # MUST be set before __setSigningPriKeyFilePath / __setSigningPriKey
242        # are called
243        self.__setSigningPriKeyPwd(signingPriKeyPwd)
244       
245        if signingPriKey is not None:
246            # Don't allow None for private key setting
247            self.__setSigningPriKey(signingPriKey)
248           
249        self.__setSigningPriKeyFilePath(signingPriKeyFilePath)
250       
251        # CA certificate(s) for verification of X.509 certificate used with
252        # signature.
253        if caCertDirPath:
254            self.caCertDirPath = caCertDirPath
255           
256        elif caCertFilePathList:
257            self.caCertFilePathList = caCertFilePathList
258       
259               
260    #_________________________________________________________________________
261    def __setReqBinSecTokValType(self, value):
262        """Set ValueType attribute for BinarySecurityToken used in a request
263         
264        @type value: string
265        @param value: name space for BinarySecurityToken ValueType check
266        'binSecTokValType' class variable for supported types.  Input can be
267        shortened to binSecTokValType keyword if desired.
268        """
269       
270        if value in self.__class__.binSecTokValType:
271            self.__reqBinSecTokValType = self.__class__.binSecTokValType[value]
272 
273        elif value in self.__class__.binSecTokValType.values():
274            self.__reqBinSecTokValType = value
275        else:
276            raise WSSecurityError, \
277                'Request BinarySecurityToken ValueType "%s" not recognised' %\
278                value
279           
280       
281    reqBinSecTokValType = property(fset=__setReqBinSecTokValType,
282         doc="ValueType attribute for BinarySecurityToken used in request")
283       
284
285    #_________________________________________________________________________
286    def __checkC14nKw(self, kw):
287        """Check keywords for canonicalization in signing process - generic
288        method for setting keywords for reference element and SignedInfo
289        element c14n
290       
291        @type kw: dict
292        @param kw: keyword used with ZSI.wstools.Utility.Canonicalization"""
293       
294        # Check for dict/None - Set to None in order to use inclusive
295        # canonicalization
296        if kw is not None and not isinstance(kw, dict):
297            # Otherwise keywords must be a dictionary
298            raise AttributeError, \
299                "Expecting dictionary type for reference c14n keywords"
300               
301        elif kw.get('unsuppressedPrefixes') and \
302             not isinstance(kw['unsuppressedPrefixes'], list) and \
303             not isinstance(kw['unsuppressedPrefixes'], tuple):
304            raise AttributeError, \
305                'Expecting list or tuple of prefix names for "%s" keyword' % \
306                'unsuppressedPrefixes'
307       
308               
309    #_________________________________________________________________________
310    def __setRefC14nKw(self, kw):
311        """Set keywords for canonicalization of reference elements in the
312        signing process"""
313        self.__checkC14nKw(kw)                   
314        self.__refC14nKw = kw
315       
316    refC14nKw = property(fset=__setRefC14nKw,
317                         doc="Keywords for c14n of reference elements")
318       
319               
320    #_________________________________________________________________________
321    def __setSignedInfoC14nKw(self, kw):
322        """Set keywords for canonicalization of SignedInfo element in the
323        signing process"""
324        self.__checkC14nKw(kw)                   
325        self.__signedInfoC14nKw = kw
326       
327    signedInfoC14nKw = property(fset=__setSignedInfoC14nKw,
328                                doc="Keywords for c14n of SignedInfo element")
329
330
331    #_________________________________________________________________________
332    def __refC14nIsExcl(self):
333        return isinstance(self.__refC14nKw, dict) and \
334               self.__refC14nKw.get('unsuppressedPrefixes') and \
335               len(self.__refC14nKw['unsuppressedPrefixes']) > 0
336               
337    refC14nIsExcl = property(fget=__refC14nIsExcl,
338    doc="Return True/False c14n for reference elements set to exclusive type")
339     
340
341    #_________________________________________________________________________
342    def __signedInfoC14nIsExcl(self):
343        return isinstance(self.__signedInfoC14nKw, dict) and \
344               self.__signedInfoC14nKw.get('unsuppressedPrefixes') and \
345               len(self.__signedInfoC14nKw['unsuppressedPrefixes']) > 0
346               
347    signedInfoC14nIsExcl = property(fget=__signedInfoC14nIsExcl,
348    doc="Return True/False c14n for SignedInfo element set to exclusive type")
349   
350   
351    #_________________________________________________________________________
352    def __setCert(self, cert):
353        """filter and convert input cert to signing verifying cert set
354        property methods.  For signingCert, set to None if it is not to be
355        included in the SOAP header.  For verifyingCert, set to None if this
356        cert can be expected to be retrieved from the SOAP header of the
357        message to be verified
358       
359        @type: ndg.security.common.X509.X509Cert / M2Crypto.X509.X509 /
360        string or None
361        @param cert: X.509 certificate. 
362       
363        @rtype ndg.security.common.X509.X509Cert
364        @return X.509 certificate object"""
365       
366        if cert is None or isinstance(cert, X509Cert):
367            # ndg.security.common.X509.X509Cert type / None
368            return cert
369           
370        elif isinstance(cert, X509.X509):
371            # M2Crypto.X509.X509 type
372            return X509Cert(m2CryptoX509=cert)
373           
374        elif isinstance(cert, basestring):
375            return X509CertParse(cert)
376       
377        else:
378            raise AttributeError, "X.509 Cert. must be type: " + \
379                "ndg.security.common.X509.X509Cert, M2Crypto.X509.X509 or " +\
380                "a base64 encoded string"
381
382   
383    #_________________________________________________________________________
384    def __getVerifyingCert(self):
385        '''Return X.509 cert object corresponding to cert used to verify the
386        signature in the last call to verify
387       
388         * Cert will correspond to one used in the LATEST call to verify, on
389         the next call it will be replaced
390         * if verify hasn't been called, the cert will be None
391       
392        @rtype: M2Crypto.X509.X509
393        @return: certificate object
394        '''
395        return self.__verifyingCert
396
397
398    #_________________________________________________________________________
399    def __setVerifyingCert(self, verifyingCert):
400        "Set property method for X.509 cert. used to verify a signature"
401        self.__verifyingCert = self.__setCert(verifyingCert)
402   
403        # Reset file path as it may no longer apply
404        self.__verifyingCertFilePath = None
405       
406    verifyingCert = property(fset=__setVerifyingCert,
407                             fget=__getVerifyingCert,
408                             doc="Set X.509 Cert. for verifying signature")
409
410
411    #_________________________________________________________________________
412    def __setVerifyingCertFilePath(self, verifyingCertFilePath):
413        "Set method for Service X.509 cert. file path property"
414       
415        if isinstance(verifyingCertFilePath, basestring):
416            self.__verifyingCert = X509CertRead(verifyingCertFilePath)
417           
418        elif verifyingCertFilePath is not None:
419            raise AttributeError, \
420            "Verifying X.509 Cert. file path must be None or a valid string"
421       
422        self.__verifyingCertFilePath = verifyingCertFilePath
423       
424    verifyingCertFilePath = property(fset=__setVerifyingCertFilePath,
425                    doc="file path of X.509 Cert. for verifying signature")
426
427
428    #_________________________________________________________________________
429    def __setSigningCert(self, signingCert):
430        "Set property method for X.509 cert. to be included with signature"
431        self.__signingCert = self.__setCert(signingCert)
432   
433        # Reset file path as it may no longer apply
434        self.__signingCertFilePath = None
435       
436    signingCert = property(fset=__setSigningCert,
437                             doc="Set X.509 Cert. to include signature")
438
439 
440    #_________________________________________________________________________
441    def __setSigningCertFilePath(self, signingCertFilePath):
442        "Set signature X.509 cert property method"
443       
444        if isinstance(signingCertFilePath, basestring):
445            self.__signingCert = X509CertRead(signingCertFilePath)
446           
447        elif signingCertFilePath is not None:
448            raise AttributeError, \
449                "Signature X.509 cert. file path must be a valid string"
450       
451        self.__signingCertFilePath = signingCertFilePath
452       
453       
454    signingCertFilePath = property(fset=__setSigningCertFilePath,
455                   doc="File path X.509 cert. to include with signed message")
456
457   
458    #_________________________________________________________________________
459    def __setSigningCertChain(self, signingCertChain):
460        '''Signature set-up with "X509PKIPathv1" BinarySecurityToken
461        ValueType.  Use an X.509 Stack to store certificates that make up a
462        chain of trust to certificate used to verify a signature
463       
464        @type signingCertChain: list or tuple of M2Crypto.X509.X509Cert or
465        ndg.security.common.X509.X509Cert types.
466        @param signingCertChain: list of certificate objects making up the
467        chain of trust.  The last certificate is the one associated with the
468        private key used to sign the message.'''
469       
470        if not isinstance(signingCertChain, list) and \
471           not isinstance(signingCertChain, tuple):
472            raise WSSecurityError, \
473                        'Expecting a list or tuple for "signingCertChain"'
474       
475        self.__signingCertChain = X509Stack()
476           
477        for cert in signingCertChain:
478            self.__signingCertChain.push(cert)
479           
480    signingCertChain = property(fset=__setSigningCertChain,
481               doc="Cert.s in chain of trust to cert. used to verify msg.")
482
483 
484    #_________________________________________________________________________
485    def __setSigningPriKeyPwd(self, signingPriKeyPwd):
486        "Set method for private key file password used to sign message"
487        if signingPriKeyPwd is not None and \
488           not isinstance(signingPriKeyPwd, basestring):
489            raise AttributeError, \
490                "Signing private key password must be None or a valid string"
491       
492        self.__signingPriKeyPwd = signingPriKeyPwd
493       
494    signingPriKeyPwd = property(fset=__setSigningPriKeyPwd,
495             doc="Password protecting private key file used to sign message")
496
497 
498    #_________________________________________________________________________
499    def __setSigningPriKey(self, signingPriKey):
500        """Set method for client private key
501       
502        Nb. if input is a string, signingPriKeyPwd will need to be set if
503        the key is password protected.
504       
505        @type signingPriKey: M2Crypto.RSA.RSA / string
506        @param signingPriKey: private key used to sign message"""
507       
508        if isinstance(signingPriKey, basestring):
509            pwdCallback = lambda *ar, **kw: self.__signingPriKeyPwd
510            self.__signingPriKey = RSA.load_key_string(signingPriKey,
511                                                       callback=pwdCallback)
512
513        elif isinstance(signingPriKey, RSA.RSA):
514            self.__signingPriKey = signingPriKey
515                   
516        else:
517            raise AttributeError, "Signing private key must be a valid " + \
518                                  "M2Crypto.RSA.RSA type or a string"
519               
520    signingPriKey = property(fset=__setSigningPriKey,
521                             doc="Private key used to sign outbound message")
522
523 
524    #_________________________________________________________________________
525    def __setSigningPriKeyFilePath(self, signingPriKeyFilePath):
526        """Set method for client private key file path
527       
528        signingPriKeyPwd MUST be set prior to a call to this method"""
529        if isinstance(signingPriKeyFilePath, basestring):                           
530            try:
531                # Read Private key to sign with   
532                priKeyFile = BIO.File(open(signingPriKeyFilePath)) 
533                pwdCallback = lambda *ar, **kw: self.__signingPriKeyPwd                                           
534                self.__signingPriKey = RSA.load_key_bio(priKeyFile, 
535                                                        callback=pwdCallback)           
536            except Exception, e:
537                raise AttributeError, \
538                                "Setting private key for signature: %s" % e
539       
540        elif signingPriKeyFilePath is not None:
541            raise AttributeError, \
542                        "Private key file path must be a valid string or None"
543       
544        self.__signingPriKeyFilePath = signingPriKeyFilePath
545       
546    signingPriKeyFilePath = property(fset=__setSigningPriKeyFilePath,
547                      doc="File path for private key used to sign message")
548
549    def __caCertIsSet(self):
550        '''Check for CA certificate set (X.509 Stack has been created)'''
551        return hasattr(self, '_SignatureHandler__caX509Stack')
552   
553    caCertIsSet = property(fget=__caCertIsSet,
554           doc='Check for CA certificate set (X.509 Stack has been created)')
555   
556    #_________________________________________________________________________
557    def __appendCAX509Stack(self, caCertList):
558        '''Store CA certificates in an X.509 Stack
559       
560        @param caCertList: list or tuple
561        @type caCertList: M2Crypto.X509.X509 certificate objects'''
562       
563        if not self.caCertIsSet:
564            self.__caX509Stack = X509Stack()
565           
566        for cert in caCertList:
567            self.__caX509Stack.push(cert)
568
569
570    #_________________________________________________________________________
571    def __setCAX509StackFromDir(self, caCertDir):
572        '''Read CA certificates from directory and add them to the X.509
573        stack
574       
575        @param caCertDir: string
576        @type caCertDir: directory from which to read CA certificate files'''
577       
578        # Mimic OpenSSL -CApath option which expects directory of CA files
579        # of form <Hash cert subject name>.0
580        reg = re.compile('\d+\.0')
581        try:
582            caCertList = [X509CertRead(caFile) \
583                          for caFile in os.listdir(caCertDir) \
584                          if reg.match(caFile)]
585        except Exception, e:
586            raise WSSecurityError, \
587                'Loading CA certificate "%s" from CA directory: %s' % \
588                                                        (caFile, str(e))
589                   
590        # Add to stack
591        self.__appendCAX509Stack(caCertList)
592       
593    caCertDirPath = property(fset=__setCAX509StackFromDir,
594                      doc="Dir. containing CA cert.s used for verification")
595
596
597    #_________________________________________________________________________
598    def __setCAX509StackFromCertFileList(self, caCertFilePathList):
599        '''Read CA certificates from file and add them to the X.509
600        stack
601       
602        @type caCertFilePathList: list or tuple
603        @param caCertFilePathList: list of file paths for CA certificates to
604        be used to verify certificate used to sign message'''
605       
606        if not isinstance(caCertFilePathList, list) and \
607           not isinstance(caCertFilePathList, tuple):
608            raise WSSecurityError, \
609                        'Expecting a list or tuple for "caCertFilePathList"'
610
611        # Mimic OpenSSL -CApath option which expects directory of CA files
612        # of form <Hash cert subject name>.0
613        try:
614            caCertList = [X509CertRead(caFile) \
615                          for caFile in caCertFilePathList]
616        except Exception, e:
617            raise WSSecurityError, \
618                    'Loading CA certificate "%s" from file list: %s' % \
619                                                        (caFile, str(e))
620                   
621        # Add to stack
622        self.__appendCAX509Stack(caCertList)
623       
624    caCertFilePathList = property(fset=__setCAX509StackFromCertFileList,
625                      doc="List of CA cert. files used for verification")
626               
627       
628    #_________________________________________________________________________
629    def sign(self, soapWriter):
630        '''Sign the message body and binary security token of a SOAP message
631       
632        @type soapWriter: ZSI.writer.SoapWriter
633        @param soapWriter: ZSI object to write SOAP message
634        '''
635       
636        # Namespaces for XPath searches
637        processorNss = \
638        {
639            'ds':     DSIG.BASE, 
640            'wsu':    _WSU.UTILITY, 
641            'wsse':   OASIS.WSSE, 
642            'soapenv':"http://schemas.xmlsoap.org/soap/envelope/" 
643        }
644
645        # Add X.509 cert as binary security token
646        if self.__reqBinSecTokValType==self.binSecTokValType['X509PKIPathv1']:
647            binSecTokVal=base64.encodestring(self.__signingCertChain.asDER())
648        else:
649            # Assume X.509 / X.509 vers 3
650            binSecTokVal = base64.encodestring(self.__signingCert.asDER())
651
652        soapWriter._header.setNamespaceAttribute('wsse', OASIS.WSSE)
653        soapWriter._header.setNamespaceAttribute('wsu', _WSU.UTILITY)
654        soapWriter._header.setNamespaceAttribute('ds', DSIG.BASE)
655       
656        if self.refC14nIsExcl or self.signedInfoC14nIsExcl:
657            soapWriter._header.setNamespaceAttribute('ec', DSIG.C14N_EXCL)
658       
659        # Check <wsse:security> isn't already present in header
660        ctxt = Context(soapWriter.dom.node, processorNss=processorNss)
661        wsseNodes = xpath.Evaluate('//wsse:security', 
662                                   contextNode=soapWriter.dom.node, 
663                                   context=ctxt)
664        if len(wsseNodes) > 1:
665            raise SignatureError, 'wsse:Security element is already present'
666
667        # Add WSSE element
668        wsseElem = soapWriter._header.createAppendElement(OASIS.WSSE, 
669                                                         'Security')
670        wsseElem.setNamespaceAttribute('wsse', OASIS.WSSE)
671       
672        # Recipient MUST parse and check this signature
673        wsseElem.node.setAttribute('SOAP-ENV:mustUnderstand', "1")
674       
675        # Binary Security Token element will contain the X.509 cert
676        # corresponding to the private key used to sing the message
677        binSecTokElem = wsseElem.createAppendElement(OASIS.WSSE, 
678                                                     'BinarySecurityToken')
679       
680        # Value type can be any be any one of those supported via
681        # binSecTokValType
682        binSecTokElem.node.setAttribute('ValueType', 
683                                        self.__reqBinSecTokValType)
684
685        encodingType = \
686"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary"
687        binSecTokElem.node.setAttribute('EncodingType', encodingType)
688       
689        # Add ID so that the binary token can be included in the signature
690        binSecTokElem.node.setAttribute('wsu:Id', "binaryToken")
691
692        binSecTokElem.createAppendTextNode(binSecTokVal)
693
694       
695        # Signature
696        signatureElem = wsseElem.createAppendElement(DSIG.BASE, 'Signature')
697        signatureElem.setNamespaceAttribute('ds', DSIG.BASE)
698       
699        # Signature - Signed Info
700        signedInfoElem = signatureElem.createAppendElement(DSIG.BASE, 
701                                                           'SignedInfo')
702       
703        # Signed Info - Canonicalization method
704        c14nMethodElem = signedInfoElem.createAppendElement(DSIG.BASE,
705                                                    'CanonicalizationMethod')
706       
707        # Set based on 'signedInfoIsExcl' property
708        c14nAlgOpt = (DSIG.C14N, DSIG.C14N_EXCL)
709        signedInfoC14nAlg = c14nAlgOpt[self.signedInfoC14nIsExcl]
710       
711        c14nMethodElem.node.setAttribute('Algorithm', signedInfoC14nAlg)
712       
713        if self.signedInfoC14nIsExcl:
714            c14nInclNamespacesElem = c14nMethodElem.createAppendElement(\
715                                                    signedInfoC14nAlg,
716                                                    'InclusiveNamespaces')
717            c14nInclNamespacesElem.node.setAttribute('PrefixList', 
718                            ' '.join(self.__signedInfoC14nKw['unsuppressedPrefixes']))
719       
720        # Signed Info - Signature method
721        sigMethodElem = signedInfoElem.createAppendElement(DSIG.BASE,
722                                                           'SignatureMethod')
723        sigMethodElem.node.setAttribute('Algorithm', DSIG.SIG_RSA_SHA1)
724       
725        # Signature - Signature value
726        signatureValueElem = signatureElem.createAppendElement(DSIG.BASE, 
727                                                             'SignatureValue')
728       
729        # Key Info
730        KeyInfoElem = signatureElem.createAppendElement(DSIG.BASE, 'KeyInfo')
731        secTokRefElem = KeyInfoElem.createAppendElement(OASIS.WSSE, 
732                                                  'SecurityTokenReference')
733       
734        # Reference back to the binary token included earlier
735        wsseRefElem = secTokRefElem.createAppendElement(OASIS.WSSE, 
736                                                        'Reference')
737        wsseRefElem.node.setAttribute('URI', "#binaryToken")
738       
739        # Add Reference to body so that it can be included in the signature
740        soapWriter.body.node.setAttribute('wsu:Id', "body")
741        soapWriter.body.node.setAttribute('xmlns:wsu', _WSU.UTILITY)
742
743        # Serialize and re-parse prior to reference generation - calculating
744        # canonicalization based on soapWriter.dom.node seems to give an
745        # error: the order of wsu:Id attribute is not correct
746        docNode = Reader().fromString(str(soapWriter))
747        ctxt = Context(docNode, processorNss=processorNss)
748        refNodes = xpath.Evaluate('//*[@wsu:Id]', 
749                                  contextNode=docNode, 
750                                  context=ctxt)
751       
752        # Set based on 'signedInfoIsExcl' property
753        refC14nAlg = c14nAlgOpt[self.refC14nIsExcl]
754       
755        # 1) Reference Generation
756        #
757        # Find references
758        for refNode in refNodes:
759           
760            # Set URI attribute to point to reference to be signed
761            #uri = u"#" + refNode.getAttribute('wsu:Id')
762            uri = u"#" + refNode.attributes[(_WSU.UTILITY, 'Id')].value
763           
764            # Canonicalize reference
765            refC14n = Canonicalize(refNode, **self.__refC14nKw)
766           
767            # Calculate digest for reference and base 64 encode
768            #
769            # Nb. encodestring adds a trailing newline char
770            digestValue = base64.encodestring(sha(refC14n).digest()).strip()
771
772
773            # Add a new reference element to SignedInfo
774            refElem = signedInfoElem.createAppendElement(DSIG.BASE, 
775                                                         'Reference')
776            refElem.node.setAttribute('URI', uri)
777           
778            # Use ds:Transforms or wsse:TransformationParameters?
779            transformsElem = refElem.createAppendElement(DSIG.BASE, 
780                                                        'Transforms')
781            transformElem = transformsElem.createAppendElement(DSIG.BASE, 
782                                                               'Transform')
783
784            # Set Canonicalization algorithm type
785            transformElem.node.setAttribute('Algorithm', refC14nAlg)
786            if self.refC14nIsExcl:
787                # Exclusive C14N requires inclusive namespace elements
788                inclNamespacesElem = transformElem.createAppendElement(\
789                                                                                   refC14nAlg,
790                                                       'InclusiveNamespaces')
791                inclNamespacesElem.node.setAttribute('PrefixList',
792                                        ' '.join(self.__refC14nKw['unsuppressedPrefixes']))
793           
794            # Digest Method
795            digestMethodElem = refElem.createAppendElement(DSIG.BASE, 
796                                                           'DigestMethod')
797            digestMethodElem.node.setAttribute('Algorithm', DSIG.DIGEST_SHA1)
798           
799            # Digest Value
800            digestValueElem = refElem.createAppendElement(DSIG.BASE, 
801                                                          'DigestValue')
802            digestValueElem.createAppendTextNode(digestValue)
803
804   
805        # 2) Signature Generation
806        #       
807        # Canonicalize the signedInfo node
808        c14nSignedInfo = Canonicalize(signedInfoElem.node, 
809                                      **self.__signedInfoC14nKw)
810
811        # Calculate digest of SignedInfo
812        signedInfoDigestValue = sha(c14nSignedInfo).digest()
813       
814        # Sign using the private key and base 64 encode the result
815        signatureValue = self.__signingPriKey.sign(signedInfoDigestValue)
816        b64EncSignatureValue = base64.encodestring(signatureValue).strip()
817
818        # Add to <SignatureValue>
819        signatureValueElem.createAppendTextNode(b64EncSignatureValue)
820
821
822    def verify(self, parsedSOAP):
823        """Verify signature
824       
825        @type parsedSOAP: ZSI.parse.ParsedSoap
826        @param parsedSOAP: object contain parsed SOAP message received from
827        sender"""
828
829        processorNss = \
830        {
831            'ds':     DSIG.BASE, 
832            'wsu':    _WSU.UTILITY, 
833            'wsse':   OASIS.WSSE, 
834            'soapenv':"http://schemas.xmlsoap.org/soap/envelope/" 
835        }
836        ctxt = Context(parsedSOAP.dom, processorNss=processorNss)
837       
838
839        signatureNodes = xpath.Evaluate('//ds:Signature', 
840                                        contextNode=parsedSOAP.dom, 
841                                        context=ctxt)
842        if len(signatureNodes) > 1:
843            raise VerifyError, 'Multiple ds:Signature elements found'
844       
845        try:
846            signatureNodes = signatureNodes[0]
847        except:
848            # Message wasn't signed
849            return
850       
851        # Two stage process: reference validation followed by signature
852        # validation
853       
854        # 1) Reference Validation
855       
856        # Check for canonicalization set via ds:CanonicalizationMethod -
857        # Use this later as a back up in case no Canonicalization was set in
858        # the transforms elements
859        c14nMethodNode = xpath.Evaluate('//ds:CanonicalizationMethod', 
860                                        contextNode=parsedSOAP.dom, 
861                                        context=ctxt)[0]
862       
863        refNodes = xpath.Evaluate('//ds:Reference', 
864                                  contextNode=parsedSOAP.dom, 
865                                  context=ctxt)
866
867        for refNode in refNodes:
868            # Get the URI for the reference
869            refURI = refNode.getAttributeNode('URI').value
870                         
871            try:
872                transformsNode = getElements(refNode, "Transforms")[0]
873                transforms = getElements(transformsNode, "Transform")
874   
875                refAlgorithm = \
876                            transforms[0].getAttributeNode("Algorithm").value
877            except Exception, e:
878                raise VerifyError, \
879            'failed to get transform algorithm for <ds:Reference URI="%s">'%\
880                        (refURI, str(e))
881               
882            # Add extra keyword for Exclusive canonicalization method
883            refC14nKw = {}
884            if refAlgorithm == DSIG.C14N_EXCL:
885                try:
886                    inclusiveNS = getElements(transforms[0], 
887                                              "InclusiveNamespaces")
888                   
889                    # Allow for no inclusive namespaces set - this is
890                    # not expected and is likely to cause problems with the
891                    # canonicalization later.
892                    if inclusiveNS:
893                        pfxListAttNode = \
894                                inclusiveNS[0].getAttributeNode('PrefixList')
895                           
896                        refC14nKw['unsuppressedPrefixes'] = \
897                                                pfxListAttNode.value.split()
898                except:
899                    raise VerifyError, \
900                'failed to handle transform (%s) in <ds:Reference URI="%s">'%\
901                        (transforms[0], refURI)
902       
903            # Canonicalize the reference data and calculate the digest
904            if refURI[0] != "#":
905                raise VerifyError, \
906                    "Expecting # identifier for Reference URI \"%s\"" % refURI
907                   
908            # XPath reference
909            uriXPath = '//*[@wsu:Id="%s"]' % refURI[1:]
910            uriNode = xpath.Evaluate(uriXPath, 
911                                     contextNode=parsedSOAP.dom, 
912                                     context=ctxt)[0]
913
914            refC14n = Canonicalize(uriNode, **refC14nKw)
915            digestValue = base64.encodestring(sha(refC14n).digest()).strip()
916           
917            # Extract the digest value that was stored           
918            digestNode = getElements(refNode, "DigestValue")[0]
919            nodeDigestValue = str(digestNode.childNodes[0].nodeValue).strip()   
920           
921            # Reference validates if the two digest values are the same
922            if digestValue != nodeDigestValue:
923                raise InvalidSignature, \
924                        'Digest Values do not match for URI: "%s"' % refURI
925               
926        # 2) Signature Validation
927        signedInfoNode = xpath.Evaluate('//ds:SignedInfo',
928                                        contextNode=parsedSOAP.dom, 
929                                        context=ctxt)[0]
930
931        # Get algorithm used for canonicalization of the SignedInfo
932        # element.  Nb. This is NOT necessarily the same as that used to
933        # canonicalize the reference elements checked above!
934        signedInfoC14nAlg = c14nMethodNode.getAttributeNode("Algorithm").value
935        signedInfoC14nKw = {}
936        if signedInfoC14nAlg == DSIG.C14N_EXCL:
937            try:
938                # Allow for no inclusive namespaces set - this is
939                # not expected and is likely to cause problems with the
940                # canonicalization later.
941                if inclusiveNS:
942                    inclusiveNS = getElements(c14nMethodNode,
943                                              "InclusiveNamespaces")
944                   
945                    pfxListAttNode = inclusiveNS[0].getAttributeNode(\
946                                                                 'PrefixList')
947                    signedInfoC14nKw['unsuppressedPrefixes'] = \
948                                                pfxListAttNode.value.split()
949                                                     
950            except Exception, e:
951                raise VerifyError, \
952            'failed to handle exclusive canonicalisation for SignedInfo: %s'%\
953                        str(e)
954
955        # Canonicalize the SignedInfo node and take digest
956        c14nSignedInfo = Canonicalize(signedInfoNode, **signedInfoC14nKw) 
957        hdrNode = Reader().fromString(Canonicalize(parsedSOAP.header))
958        hdrCtxt = Context(hdrNode, processorNss=processorNss)
959        signedInfoNode2 = xpath.Evaluate('//ds:SignedInfo',
960                                        contextNode=hdrNode, 
961                                        context=hdrCtxt)[0]
962        c14nSignedInfo2 = Canonicalize(signedInfoNode, **signedInfoC14nKw)
963        signedInfoDigestValue2 = sha(c14nSignedInfo2).digest()
964                             
965        signedInfoDigestValue = sha(c14nSignedInfo).digest()
966       
967        # Get the signature value in order to check against the digest just
968        # calculated
969        signatureValueNode = xpath.Evaluate('//ds:SignatureValue',
970                                            contextNode=parsedSOAP.dom, 
971                                            context=ctxt)[0]
972
973        # Remove base 64 encoding
974        # This line necessary? - only decode call needed??  pyGridWare vers
975        # seems to preserve whitespace
976        b64EncSignatureValue = \
977                    str(signatureValueNode.childNodes[0].nodeValue).strip()
978       
979        signatureValue = base64.decodestring(b64EncSignatureValue)
980
981
982        # Look for X.509 Cert in wsse:BinarySecurityToken node
983        try:
984            binSecTokNode = xpath.Evaluate('//wsse:BinarySecurityToken',
985                                           contextNode=parsedSOAP.dom,
986                                           context=ctxt)[0]
987        except:
988            # Signature may not have included the Binary Security Token in
989            # which case the verifying cert will need to have been set
990            # elsewhere
991            binSecTokNode = None
992            pass 
993       
994        #import pdb;pdb.set_trace()
995        if binSecTokNode:
996            try:
997                x509CertTxt=str(binSecTokNode.childNodes[0]._get_nodeValue())
998               
999                valueType = binSecTokNode.getAttributeNode("ValueType").value
1000                if valueType in (self.__class__.binSecTokValType['X509v3'],
1001                                 self.__class__.binSecTokValType['X509']):
1002               
1003                    # Convert parsed cert text into PEM form that can be read
1004                    # by X.509 string parser
1005                    #
1006                    # Check for line breaks at 64th byte as expected for PEM
1007                    # encoding
1008                    if x509CertTxt[64] != "\n":
1009                        # Check for other whitespace and reomve
1010                        # Expecting cert split into lines of length 64 bytes
1011                        x509CertTxt = re.sub('\s', '', x509CertTxt)
1012                       
1013                        # Split into lines of length 64
1014                        x509CertSpl = re.split('(.{64})', x509CertTxt)
1015                        x509CertTxt = '\n'.join([i for i in x509CertSpl if i])
1016                       
1017                    b64EncX509Cert = self.__class__.__beginCert+x509CertTxt+\
1018                             self.__class__.__endCert
1019                    self.__setVerifyingCert(b64EncX509Cert)
1020                   
1021                    x509Stack = X509Stack()
1022
1023                elif valueType == \
1024                    self.__class__.binSecTokValType['X509PKIPathv1']:
1025                   
1026                    derString = base64.decodestring(x509CertTxt)
1027                    x509Stack = X509StackParseFromDER(derString)
1028                   
1029                    # TODO: Check ordering - is the last off the stack the
1030                    # one to use to verify the message?
1031                    self.__verifyingCert = x509Stack[-1]
1032                else:
1033                    raise WSSecurityError, "BinarySecurityToken ValueType " +\
1034                        'attribute is not recognised: "%s"' % valueType
1035                               
1036            except Exception, e:
1037                raise VerifyError, "Error extracting BinarySecurityToken " + \
1038                                   "from WSSE header: " + str(e)
1039
1040        if self.__verifyingCert is None:
1041            raise VerifyError, "No certificate set for verification " + \
1042                "of the signature"
1043       
1044        # Extract RSA public key from the cert
1045        rsaPubKey = self.__verifyingCert.pubKey.get_rsa()
1046
1047        # Apply the signature verification
1048        try:
1049            verify = rsaPubKey.verify(signedInfoDigestValue, signatureValue)
1050        except RSA.RSAError, e:
1051            raise VerifyError, "Error in Signature: " + str(e)
1052       
1053        if not verify:
1054            raise InvalidSignature, "Invalid signature"
1055       
1056        # Verify chain of trust
1057        x509Stack.verifyCertChain(x509Cert2Verify=self.__verifyingCert,
1058                                  caX509Stack=self.__caX509Stack)
1059           
1060        #print "Signature OK"
1061
1062
1063class EncryptionError(Exception):
1064    """Flags an error in the encryption process"""
1065
1066class DecryptionError(Exception):
1067    """Raised from EncryptionHandler.decrypt if an error occurs with the
1068    decryption process"""
1069
1070
1071class EncryptionHandler(object):
1072    """Encrypt/Decrypt SOAP messages using WS-Security""" 
1073   
1074    # Map namespace URIs to Crypto algorithm module and mode
1075    cryptoAlg = \
1076    {
1077         _ENCRYPTION.WRAP_AES256:      {'module':       AES, 
1078                                        'mode':         AES.MODE_ECB,
1079                                        'blockSize':    16},
1080         
1081         # CBC (Cipher Block Chaining) modes
1082         _ENCRYPTION.BLOCK_AES256:     {'module':       AES, 
1083                                        'mode':         AES.MODE_CBC,
1084                                        'blockSize':    16},
1085                                       
1086         _ENCRYPTION.BLOCK_TRIPLEDES:  {'module':       DES3, 
1087                                        'mode':         DES3.MODE_CBC,
1088                                        'blockSize':    8}   
1089    }
1090
1091     
1092    def __init__(self,
1093                 signingCertFilePath=None, 
1094                 signingPriKeyFilePath=None, 
1095                 signingPriKeyPwd=None,
1096                 chkSecurityTokRef=False,
1097                 encrNS=_ENCRYPTION.BLOCK_AES256):
1098       
1099        self.__signingCertFilePath = signingCertFilePath
1100        self.__signingPriKeyFilePath = signingPriKeyFilePath
1101        self.__signingPriKeyPwd = signingPriKeyPwd
1102       
1103        self.__chkSecurityTokRef = chkSecurityTokRef
1104       
1105        # Algorithm for shared key encryption
1106        try:
1107            self.__encrAlg = self.cryptoAlg[encrNS]
1108           
1109        except KeyError:
1110            raise EncryptionError, \
1111        'Input encryption algorithm namespace "%s" is not supported' % encrNS
1112
1113        self.__encrNS = encrNS
1114       
1115       
1116    def encrypt(self, soapWriter):
1117        """Encrypt an outbound SOAP message
1118       
1119        Use Key Wrapping - message is encrypted using a shared key which
1120        itself is encrypted with the public key provided by the X.509 cert.
1121        signingCertFilePath"""
1122       
1123        # Use X.509 Cert to encrypt
1124        x509Cert = X509.load_cert(self.__signingCertFilePath)
1125       
1126        soapWriter.dom.setNamespaceAttribute('wsse', OASIS.WSSE)
1127        soapWriter.dom.setNamespaceAttribute('xenc', _ENCRYPTION.BASE)
1128        soapWriter.dom.setNamespaceAttribute('ds', DSIG.BASE)
1129       
1130        # TODO: Put in a check to make sure <wsse:security> isn't already
1131        # present in header
1132        wsseElem = soapWriter._header.createAppendElement(OASIS.WSSE, 
1133                                                         'Security')
1134        wsseElem.node.setAttribute('SOAP-ENV:mustUnderstand', "1")
1135       
1136        encrKeyElem = wsseElem.createAppendElement(_ENCRYPTION.BASE, 
1137                                                   'EncryptedKey')
1138       
1139        # Encryption method used to encrypt the shared key
1140        keyEncrMethodElem = encrKeyElem.createAppendElement(_ENCRYPTION.BASE, 
1141                                                        'EncryptionMethod')
1142       
1143        keyEncrMethodElem.node.setAttribute('Algorithm', 
1144                                            _ENCRYPTION.KT_RSA_1_5)
1145
1146
1147        # Key Info
1148        KeyInfoElem = encrKeyElem.createAppendElement(DSIG.BASE, 'KeyInfo')
1149       
1150        secTokRefElem = KeyInfoElem.createAppendElement(OASIS.WSSE, 
1151                                                  'SecurityTokenReference')
1152       
1153        x509IssSerialElem = secTokRefElem.createAppendElement(DSIG.BASE, 
1154                                                          'X509IssuerSerial')
1155
1156       
1157        x509IssNameElem = x509IssSerialElem.createAppendElement(DSIG.BASE, 
1158                                                          'X509IssuerName')
1159        x509IssNameElem.createAppendTextNode(x509Cert.get_issuer().as_text())
1160
1161       
1162        x509IssSerialNumElem = x509IssSerialElem.createAppendElement(
1163                                                  DSIG.BASE, 
1164                                                  'X509IssuerSerialNumber')
1165       
1166        x509IssSerialNumElem.createAppendTextNode(
1167                                          str(x509Cert.get_serial_number()))
1168
1169        # References to what has been encrypted
1170        encrKeyCiphDataElem = encrKeyElem.createAppendElement(
1171                                                          _ENCRYPTION.BASE,
1172                                                          'CipherData')
1173       
1174        encrKeyCiphValElem = encrKeyCiphDataElem.createAppendElement(
1175                                                          _ENCRYPTION.BASE,
1176                                                          'CipherValue')
1177
1178        # References to what has been encrypted
1179        refListElem = encrKeyElem.createAppendElement(_ENCRYPTION.BASE,
1180                                                      'ReferenceList')
1181       
1182        dataRefElem = refListElem.createAppendElement(_ENCRYPTION.BASE,
1183                                                      'DataReference')
1184        dataRefElem.node.setAttribute('URI', "#encrypted")
1185
1186                     
1187        # Add Encrypted data to SOAP body
1188        encrDataElem = soapWriter.body.createAppendElement(_ENCRYPTION.BASE, 
1189                                                           'EncryptedData')
1190        encrDataElem.node.setAttribute('Id', 'encrypted')
1191        encrDataElem.node.setAttribute('Type', _ENCRYPTION.BASE) 
1192             
1193        # Encryption method used to encrypt the target data
1194        dataEncrMethodElem = encrDataElem.createAppendElement(
1195                                                      _ENCRYPTION.BASE, 
1196                                                      'EncryptionMethod')
1197       
1198        dataEncrMethodElem.node.setAttribute('Algorithm', self.__encrNS)
1199       
1200        # Cipher data
1201        ciphDataElem = encrDataElem.createAppendElement(_ENCRYPTION.BASE,
1202                                                        'CipherData')
1203       
1204        ciphValueElem = ciphDataElem.createAppendElement(_ENCRYPTION.BASE,
1205                                                         'CipherValue')
1206
1207
1208        # Get elements from SOAP body for encryption
1209        dataElem = soapWriter.body.node.childNodes[0]
1210        data = dataElem.toxml()
1211     
1212        # Pad data to nearest multiple of encryption algorithm's block size   
1213        modData = len(data) % self.__encrAlg['blockSize']
1214        nPad = modData and self.__encrAlg['blockSize'] - modData or 0
1215       
1216        # PAd with random junk but ...
1217        data += os.urandom(nPad-1)
1218       
1219        # Last byte should be number of padding bytes
1220        # (http://www.w3.org/TR/xmlenc-core/#sec-Alg-Block)
1221        data += chr(nPad)       
1222       
1223        # Generate shared key and input vector - for testing use hard-coded
1224        # values to allow later comparison             
1225        sharedKey = os.urandom(self.__encrAlg['blockSize'])
1226        iv = os.urandom(self.__encrAlg['blockSize'])
1227       
1228        alg = self.__encrAlg['module'].new(sharedKey,
1229                                           self.__encrAlg['mode'],
1230                                           iv)
1231 
1232        # Encrypt required elements - prepend input vector
1233        encryptedData = alg.encrypt(iv + data)
1234        dataCiphValue = base64.encodestring(encryptedData).strip()
1235
1236        ciphValueElem.createAppendTextNode(dataCiphValue)
1237       
1238       
1239        # ! Delete unencrypted message body elements !
1240        soapWriter.body.node.removeChild(dataElem)
1241
1242       
1243        # Use X.509 cert public key to encrypt the shared key - Extract key
1244        # from the cert
1245        rsaPubKey = x509Cert.get_pubkey().get_rsa()
1246       
1247        # Encrypt the shared key
1248        encryptedSharedKey = rsaPubKey.public_encrypt(sharedKey, 
1249                                                      RSA.pkcs1_padding)
1250       
1251        encrKeyCiphVal = base64.encodestring(encryptedSharedKey).strip()
1252       
1253        # Add the encrypted shared key to the EncryptedKey section in the SOAP
1254        # header
1255        encrKeyCiphValElem.createAppendTextNode(encrKeyCiphVal)
1256
1257#        print soapWriter.dom.node.toprettyxml()
1258#        import pdb;pdb.set_trace()
1259       
1260       
1261    def decrypt(self, parsedSOAP):
1262        """Decrypt an inbound SOAP message"""
1263       
1264        processorNss = \
1265        {
1266            'xenc':   _ENCRYPTION.BASE,
1267            'ds':     DSIG.BASE, 
1268            'wsu':    _WSU.UTILITY, 
1269            'wsse':   OASIS.WSSE, 
1270            'soapenv':"http://schemas.xmlsoap.org/soap/envelope/" 
1271        }
1272        ctxt = Context(parsedSOAP.dom, processorNss=processorNss)
1273       
1274        refListNodes = xpath.Evaluate('//xenc:ReferenceList', 
1275                                      contextNode=parsedSOAP.dom, 
1276                                      context=ctxt)
1277        if len(refListNodes) > 1:
1278            raise DecryptionError, 'Expecting a single ReferenceList element'
1279       
1280        try:
1281            refListNode = refListNodes[0]
1282        except:
1283            # Message wasn't encrypted - is this OK or is a check needed for
1284            # encryption info in SOAP body - enveloped form?
1285            return
1286
1287
1288        # Check for wrapped key encryption
1289        encrKeyNodes = xpath.Evaluate('//xenc:EncryptedKey', 
1290                                      contextNode=parsedSOAP.dom, 
1291                                      context=ctxt)
1292        if len(encrKeyNodes) > 1:
1293            raise DecryptionError, 'This implementation can only handle ' + \
1294                                   'single EncryptedKey element'
1295       
1296        try:
1297            encrKeyNode = encrKeyNodes[0]
1298        except:
1299            # Shared key encryption used - leave out for the moment
1300            raise DecryptionError, 'This implementation can only handle ' + \
1301                                   'wrapped key encryption'
1302
1303       
1304        # Check encryption method
1305        keyEncrMethodNode = getElements(encrKeyNode, 'EncryptionMethod')[0]     
1306        keyAlgorithm = keyEncrMethodNode.getAttributeNode("Algorithm").value
1307        if keyAlgorithm != _ENCRYPTION.KT_RSA_1_5:
1308            raise DecryptionError, \
1309            'Encryption algorithm for wrapped key is "%s", expecting "%s"' % \
1310                (keyAlgorithm, _ENCRYPTION.KT_RSA_1_5)
1311
1312                                                           
1313        if self.__chkSecurityTokRef and self.__signingCertFilePath:
1314             
1315            # Check input cert. against SecurityTokenReference
1316            securityTokRefXPath = '/ds:KeyInfo/wsse:SecurityTokenReference'
1317            securityTokRefNode = xpath.Evaluate(securityTokRefXPath, 
1318                                                contextNode=encrKeyNode, 
1319                                                context=ctxt)
1320            # TODO: Look for ds:X509* elements to check against X.509 cert
1321            # input
1322
1323
1324        # Look for cipher data for wrapped key
1325        keyCiphDataNode = getElements(encrKeyNode, 'CipherData')[0]
1326        keyCiphValNode = getElements(keyCiphDataNode, 'CipherValue')[0]
1327
1328        keyCiphVal = str(keyCiphValNode.childNodes[0].nodeValue)
1329        encryptedKey = base64.decodestring(keyCiphVal)
1330
1331        # Read RSA Private key in order to decrypt wrapped key 
1332        priKeyFile = BIO.File(open(self.__signingPriKeyFilePath))         
1333        pwdCallback = lambda *ar, **kw: self.__signingPriKeyPwd                                       
1334        priKey = RSA.load_key_bio(priKeyFile, callback=pwdCallback)
1335       
1336        sharedKey = priKey.private_decrypt(encryptedKey, RSA.pkcs1_padding)
1337       
1338
1339        # Check list of data elements that have been encrypted
1340        for dataRefNode in refListNode.childNodes:
1341
1342            # Get the URI for the reference
1343            dataRefURI = dataRefNode.getAttributeNode('URI').value                           
1344            if dataRefURI[0] != "#":
1345                raise VerifyError, \
1346                    "Expecting # identifier for DataReference URI \"%s\"" % \
1347                    dataRefURI
1348
1349            # XPath reference - need to check for wsu namespace qualified?
1350            #encrNodeXPath = '//*[@wsu:Id="%s"]' % dataRefURI[1:]
1351            encrNodeXPath = '//*[@Id="%s"]' % dataRefURI[1:]
1352            encrNode = xpath.Evaluate(encrNodeXPath, 
1353                                      contextNode=parsedSOAP.dom, 
1354                                      context=ctxt)[0]
1355               
1356            dataEncrMethodNode = getElements(encrNode, 'EncryptionMethod')[0]     
1357            dataAlgorithm = \
1358                        dataEncrMethodNode.getAttributeNode("Algorithm").value
1359            try:       
1360                # Match algorithm name to Crypto module
1361                CryptoAlg = self.cryptoAlg[dataAlgorithm]
1362               
1363            except KeyError:
1364                raise DecryptionError, \
1365'Encryption algorithm for data is "%s", supported algorithms are:\n "%s"' % \
1366                    (keyAlgorithm, "\n".join(self.cryptoAlg.keys()))
1367
1368            # Get Data
1369            dataCiphDataNode = getElements(encrNode, 'CipherData')[0]
1370            dataCiphValNode = getElements(dataCiphDataNode, 'CipherValue')[0]
1371       
1372            dataCiphVal = str(dataCiphValNode.childNodes[0].nodeValue)
1373            encryptedData = base64.decodestring(dataCiphVal)
1374           
1375            alg = CryptoAlg['module'].new(sharedKey, CryptoAlg['mode'])
1376            decryptedData = alg.decrypt(encryptedData)
1377           
1378            # Strip prefix - assume is block size
1379            decryptedData = decryptedData[CryptoAlg['blockSize']:]
1380           
1381            # Strip any padding suffix - Last byte should be number of padding
1382            # bytes
1383            # (http://www.w3.org/TR/xmlenc-core/#sec-Alg-Block)
1384            lastChar = decryptedData[-1]
1385            nPad = ord(lastChar)
1386           
1387            # Sanity check - there may be no padding at all - the last byte
1388            # being the end of the encrypted XML?
1389            #
1390            # TODO: are there better sanity checks than this?!
1391            if nPad < CryptoAlg['blockSize'] and nPad > 0 and \
1392               lastChar != '\n' and lastChar != '>':
1393               
1394                # Follow http://www.w3.org/TR/xmlenc-core/#sec-Alg-Block -
1395                # last byte gives number of padding bytes
1396                decryptedData = decryptedData[:-nPad]
1397
1398
1399            # Parse the encrypted data - inherit from Reader as a fudge to
1400            # enable relevant namespaces to be added prior to parse
1401            processorNss.update({'xsi': SCHEMA.XSI3, 'ns1': 'urn:ZSI:examples'})
1402            class _Reader(Reader):
1403                def initState(self, ownerDoc=None):
1404                    Reader.initState(self, ownerDoc=ownerDoc)
1405                    self._namespaces.update(processorNss)
1406                   
1407            rdr = _Reader()
1408            dataNode = rdr.fromString(decryptedData, ownerDoc=parsedSOAP.dom)
1409           
1410            # Add decrypted element to parent and remove encrypted one
1411            parentNode = encrNode._get_parentNode()
1412            parentNode.appendChild(dataNode)
1413            parentNode.removeChild(encrNode)
1414           
1415            from xml.dom.ext import ReleaseNode
1416            ReleaseNode(encrNode)
1417           
1418            # Ensure body_root attribute is up to date in case it was
1419            # previously encrypted
1420            parsedSOAP.body_root = parsedSOAP.body.childNodes[0]
1421            #print decryptedData
1422            #import pdb;pdb.set_trace()
1423
1424
1425#_____________________________________________________________________________
1426from zope.interface import classProvides, implements, Interface
1427import twisted.web.http
1428from twisted.python import log, failure
1429
1430from ZSI.twisted.WSresource import DefaultHandlerChain, \
1431    DefaultCallbackHandler, CallbackChainInterface, HandlerChainInterface, \
1432    DataHandler
1433   
1434from ZSI import _get_element_nsuri_name, EvaluateException, ParseException
1435   
1436   
1437class WSSecurityHandlerChainFactory:
1438    protocol = DefaultHandlerChain
1439   
1440    @classmethod
1441    def newInstance(cls):
1442        return cls.protocol(DefaultCallbackHandler, 
1443                            DataHandler,
1444                            WSSecurityHandler)
1445   
1446
1447class WSSecurityHandler:
1448    classProvides(HandlerChainInterface)
1449
1450    signatureHandler = None
1451   
1452    @classmethod
1453    def processRequest(cls, ps, **kw):
1454        """invokes callback that should return a (request,response) tuple.
1455        representing the SOAP request and response respectively.
1456        ps -- ParsedSoap instance representing HTTP Body.
1457        request -- twisted.web.server.Request
1458        """
1459        if cls.signatureHandler:
1460            cls.signatureHandler.verify(ps)
1461           
1462        return ps
1463   
1464    @classmethod
1465    def processResponse(cls, sw, **kw):
1466       
1467        if cls.signatureHandler:
1468            cls.signatureHandler.sign(sw)
1469           
1470        return sw
Note: See TracBrowser for help on using the repository browser.