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

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