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

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

Fixes to tickets #828 #829

ndg.security.server/ndg/security/server/AttAuthority/server-config.tac,
ndg.security.server/ndg/security/server/ca/server-config.tac,
ndg.security.server/ndg/security/server/SessionMgr/server-config.tac:

  • Split import for wsSecurity into common component (no Zope or Twisted) and

server component (Zope and Twisted imports)

ndg.security.server/ndg/security/server/twisted.py:

  • new module to contain Twisted handler code for NDG-Security server egg.

This code is removed from common.wsSecurity so that the client egg no longer
has any Twisted or Zope dependencies

ndg.security.common/ndg/security/common/wsSecurity.py: removed Twisted handler
code and moved to new server.twisted module

ndg-security-install.py: add new --config-dir option to copy server egg conf/
dir contents to /etc/ndg/security on target host. Incomplete as a way to
programmatically get the egg site-packages/ location is needed.

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