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

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

raise new NoSignatureFound? if signatureNode not present in SignatureHandler?.verify

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