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

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

Added Timestamp capability to wsSecurity module. Currently testing against Apache2 Rampart.

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