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

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

ndg.security.server/ndg/security/server/AttAuthority/server-config.tac:
fix to caCertFilePathList input to SignatureHandler?. Correctly initialise
if not set.

ndg.security.server/ndg/security/server/AttAuthority/init.py:
Corrected error message text for where a user is not registered or no
mapping is available: ref. userId rather than AC holder DN to allow for the
case in DEWS where a userId distinct from a Proxy cert. DN is used.

ndg.security.test/ndg/security/test/AttAuthority/AttAuthorityClientTest.py:
added test8GetMappedAttCertStressTest test for WebSphere? integration tests.
It makes multiple calls with different ACs input to check for errors in
signature or verification.

ndg.security.test/ndg/security/test/AttAuthority/attAuthorityClientTest.cfg:
added additional config params for the above.

ndg.security.test/ndg/security/test/MyProxy/myProxyProperties.xml and
ndg.security.test/ndg/security/test/MyProxy/myProxyClientTest.cfg:
switched cert ID of test machine.

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

  • new X509Cert methods asDER and asPEM to convert to these formats.

toString now calls to asPEM

  • new class X509Stack to wrap M2Crypto.X509.X509_Stack. This includes an

extra method, verifyCertChain, to verify a chain of trust in the certs
contained in the stack.

  • standalone function, X509StackParseFromDER, wraps

M2Crypto.X509.new_stack_from_der

  • fix to X500DN class to enable correct parsing of proxy certificate DNs.

These have multiple CN entries. These are represented by changing the CN
dict entry to a tuple when initialised.

ndg.security.common/ndg/security/common/wsSecurity.py: changes to enable
handling of certificate chains in WSSE BinarySecurityToken? elements. This
will enable use of proxy certificates with signatures as their chain of
trust is proxy cert -> user cert -> CA cert rather than just cert -> CA cert.

types.

BinarySecurityToken? ValueType? to use

  • SignatureHandler?.init includes new signingCertChain keyword.
  • signingCertChain attribute of class enables setting of an X509Stack object

to assign to BinarySecurityToken?.

then Base 64 encode rather than converting into PEM and then having to
strip BEGIN CERT / END CERT delimiters.

to enable check of Canonicalization - REMOVE in future check in.

BinarySecurityToken? ValueTypes? - 'X509PKIPathv1', 'X509' and 'X509v3'

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