source: TI12-security/trunk/python/ndg.security.common/ndg/security/common/wssecurity/etree.py @ 4081

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

First working version of an OpenID Provider as opposed to a Relying Party as avail. with AuthKit?. The code is taken from the HTTPServer example in the Python OpenID package and refactored into WSGI middleware.

  • ndg.security.server.wsgi.openid_provider - WSGI middleware package
  • Tests/openid-provider/op: pylons project test harness for the above

TODO: integrate into AuthKit? and Beaker Session Middleware as required.

Line 
1"""WS-Security digital signature handler for ElementTree XML package
2
3NERC Data Grid Project
4"""
5__author__ = "P J Kershaw"
6__date__ = "02/07/08"
7__copyright__ = "(C) 2008 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: wsSecurity.py 3790 2008-04-17 12:46:30Z pjkersha $'
13
14import re
15
16# Digest and signature/verify
17from sha import sha
18from M2Crypto import X509, BIO, RSA
19import base64
20
21from elementtree import ElementTree, ElementC14N
22from StringIO import StringIO
23
24# Conditional import as this is required for the encryption
25# handler
26try:
27    # For shared key encryption
28    from Crypto.Cipher import AES, DES3
29except:
30    from warnings import warn
31    warn('Crypto.Cipher not available: EncryptionHandler disabled!',
32         RuntimeWarning)
33    class AES:
34        MODE_ECB = None
35        MODE_CBC = None
36       
37    class DES3: 
38        MODE_CBC = None
39
40import os
41
42import ZSI
43from ZSI.wstools.Namespaces import DSIG, ENCRYPTION, WSU, WSA200403, \
44                                   SOAP, SCHEMA # last included for xsi
45                                   
46from ZSI.wstools.Namespaces import WSU as _WSU
47from ZSI.wstools.Namespaces import OASIS as _OASIS
48                                 
49from ZSI.TC import ElementDeclaration,TypeDefinition
50from ZSI.generate.pyclass import pyclass_type
51
52from ZSI.wstools.Utility import DOMException
53from ZSI.wstools.Utility import NamespaceError, MessageInterface, ElementProxy
54
55# Canonicalization
56from ZSI.wstools.c14n import Canonicalize
57
58from xml.dom import Node
59from xml.xpath.Context import Context
60from xml import xpath
61
62# Enable settings from a config file
63from ndg.security.common.wssecurity import WSSecurityConfig
64
65# Check SoapWriter.dom attribute is ElementTreeProxy type
66from ndg.security.common.zsi_utils.elementtreeproxy import ElementTreeProxy
67
68from ndg.security.common.X509 import X509Cert, X509CertParse, X509CertRead, \
69X509Stack, X509StackParseFromDER
70
71from datetime import datetime, timedelta
72import logging
73log = logging.getLogger(__name__)
74
75
76class _ENCRYPTION(ENCRYPTION):
77    '''Derived from ENCRYPTION class to add in extra 'tripledes-cbc' - is this
78    any different to 'des-cbc'?  ENCRYPTION class implies that it is the same
79    because it's assigned to 'BLOCK_3DES' ??'''
80    BLOCK_TRIPLEDES = "http://www.w3.org/2001/04/xmlenc#tripledes-cbc"
81
82class WSU(_WSU):
83    '''Try different utility namespace for use with WebSphere'''
84    #UTILITY = "http://schemas.xmlsoap.org/ws/2003/06/utility"
85    UTILITY = \
86"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"
87
88class OASIS(_OASIS):
89    # wss4j 1.5.3
90    WSSE11 = \
91    "http://docs.oasis-open.org/wss/oasis-wss-wssecurity-secext-1.1.xsd"
92    # wss4j 1.5.1
93    #WSSE11 = "http://docs.oasis-open.org/wss/2005/xx/oasis-2005xx-wss-wssecurity-secext-1.1.xsd"
94       
95def getElements(node, nameList):
96    '''DOM Helper function for getting child elements from a given node'''
97    # Avoid sub-string matches
98    nameList = isinstance(nameList, basestring) and [nameList] or nameList
99    return [n for n in node.childNodes if str(n.localName) in nameList]
100
101
102def getChildNodes(node, nodeList=None):
103    if nodeList is None:
104        nodeList = [node] 
105    return _getChildNodes(node, nodeList)
106           
107def _getChildNodes(node, nodeList):
108
109    if node.attributes is not None:
110        nodeList += node.attributes.values() 
111    nodeList += node.childNodes
112    for childNode in node.childNodes:
113        _getChildNodes(childNode, nodeList)
114    return nodeList
115
116
117class WSSecurityError(Exception):
118    """For WS-Security generic exceptions not covered by other exception
119    classes in this module"""
120
121class InvalidCertChain(WSSecurityError):   
122    """Raised from SignatureHandler.verify if the certificate submitted to
123    verify a signature is not from a known CA"""
124   
125class VerifyError(WSSecurityError):
126    """Raised from SignatureHandler.verify if an error occurs in the signature
127    verification"""
128
129class NoSignatureFound(WSSecurityError): 
130    """Incoming message to be verified was not signed"""
131   
132class TimestampError(WSSecurityError):
133    """Raised from SignatureHandler._verifyTimestamp if there is a problem with
134    the created or expiry times in an input message Timestamp"""
135   
136class InvalidSignature(WSSecurityError):
137    """Raised from verify method for an invalid signature"""
138
139class SignatureError(WSSecurityError):
140    """Flag if an error occurs during signature generation"""
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    binSecTokEncType = \
170"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary"
171
172    # Namespaces for XPath searches
173    _processorNSs = \
174    {
175        'ds':       DSIG.BASE, 
176        'wsu':      WSU.UTILITY, 
177        'wsse':     OASIS.WSSE, 
178        'SOAP-ENV': SOAP.ENV
179#        'soapenv':  SOAP.ENV
180    }
181
182
183    #_________________________________________________________________________
184    def __init__(self, cfgFilePath=None, cfgFileSection='DEFAULT',
185                 cfgClass=WSSecurityConfig, **kw):
186        '''
187        @reqBinSecTokValType: set the ValueType for the BinarySecurityToken
188        added to the WSSE header for a signed message.  See
189        __setReqBinSecTokValType method and binSecTokValType class variable
190        for options.  binSecTokValType determines whether signingCert or
191        signingCertChain attributes will be used.       
192        @type binSecTokValType: string
193       
194        @param verifyingCert: X.509 certificate used by verify method to
195        verify a message.  This argument can be omitted if the message to
196        be verified contains the X.509 certificate in the
197        BinarySecurityToken element.  In this case, the cert read from the
198        message will be assigned to the verifyingCert attribute.
199        @type verifyingCert: M2Crypto.X509.X509 /
200        ndg.security.common.X509.X509Cert
201       
202        @param verifyingCertFilePath: alternative input to the above, pass
203        the file path to the certificate stored in a file
204        @type verifyingCertFilePath: string
205       
206        @param signingCert: certificate associated with private key used to
207        sign a message.  The sign method will add this to the
208        BinarySecurityToken element of the WSSE header.  binSecTokValType
209        attribute must be set to 'X509' or 'X509v3' ValueTyep.  As an
210        alternative, use signingCertChain - see below...
211        @type signingCert: M2Crypto.X509.X509 /
212        ndg.security.common.X509.X509Cert
213       
214        @param signingCertFilePath: alternative input to the above, pass
215        the file path to the certificate stored in a file
216        @type signingCertFilePath: string
217       
218        @param signingCertChain: pass a list of certificates constituting a
219        chain of trust from the certificate used to verifying the signature
220        backward to the CA cert.  The CA cert need not be included.  To use
221        this option, reqBinSecTokValType must be set to the 'X509PKIPathv1'
222        ValueType
223        @type signingCertChain: list or tuple
224       
225        @param signingPriKey: private key used to be sign method to sign
226        message
227        @type signingPriKey: M2Crypto.RSA.
228       
229        @param signingPriKeyFilePath: equivalent to the above but pass
230        private key from PEM file
231        @type signingPriKeyFilePath: string
232       
233        @param signingPriKeyPwd: password protecting private key.  Set /
234        default to None if there is no password.
235        @type signingPriKeyPwd: string or None
236       
237        @param caCertDirPath: establish trust for signature verification.
238        This is a directory containing CA certificates.  These are used to
239        verify the certificate used to verify the message signature.
240        @type caCertDirPath: string
241       
242        @param caCertFilePathList: same as above except pass in a list of
243        file paths instead of a single directory name.
244        @type caCertFilePathList: list or tuple
245       
246        @param addTimestamp: set to true to add a timestamp to outbound
247        messages
248        @type addTimestamp: bool
249       
250        @param : for servers - set this flag to enable the signature value of a
251        request to be recorded and included with a SignatureConfirmation
252        element in the response.
253        @type : bool
254       
255        @param refC14nKw: dictionary of keywords to reference
256        Canonicalization.  Use 'inclusive_namespaces' keyword to set
257        inclusive_namespaces.
258        @type refC14nKw: dict
259       
260        @param signedInfoC14nKw: keywords to Signed Info Canonicalization.
261        It uses the same format as refC14nKw above.
262        @type signedInfoC14nKw: dict
263        '''
264        log.debug("SignatureHandler.__init__ ...")
265       
266        # WSSecurityConfig is the default class for reading config params but
267        # alternative derivative class may be passed in instead.
268        if not issubclass(cfgClass, WSSecurityConfig):
269            raise TypeError("%s is not a sub-class of WSSecurityConfig" % \
270                            cfgClass)
271        self.cfg = cfgClass()
272       
273        # Read parameters from config file if set
274        if cfgFilePath:
275            log.debug("SignatureHandler.__init__: Processing config file...")
276            self.cfg.read(cfgFilePath)
277            self.cfg.parse(section=cfgFileSection)
278       
279        # Also update config from keywords set
280        log.debug("SignatureHandler.__init__: setting config from keywords...")
281        self.cfg.update(kw)
282       
283       
284        self.__setReqBinSecTokValType(self.cfg['reqBinSecTokValType'])
285       
286        # Set keywords for canonicalization of SignedInfo and reference
287        # elements
288        # TODO: get rid of refC14nKw and signedInfoC14nKw options
289        if len(self.cfg.get('refC14nInclNS', [])):
290            self.__setRefC14nKw({'inclusive_namespaces':
291                                 self.cfg['refC14nInclNS']})
292        else:
293            self.__setRefC14nKw(self.cfg['refC14nKw'])
294
295   
296        if len(self.cfg.get('signedInfoC14nNS', [])):
297            self.__setSignedInfoC14nKw({'inclusive_namespaces':
298                                        self.cfg['signedInfoC14nNS']})
299        else:
300            self.__setSignedInfoC14nKw(self.cfg['signedInfoC14nKw'])
301           
302
303        self.__setVerifyingCert(self.cfg['verifyingCert'])
304        self.__setVerifyingCertFilePath(self.cfg['verifyingCertFilePath'])
305       
306        self.__setSigningCert(self.cfg['signingCert'])
307        self.__setSigningCertFilePath(self.cfg['signingCertFilePath'])
308
309        if self.cfg.get('signingCertChain'):
310            self.__setSigningCertChain(self.cfg['signingCertChain'])
311        else:
312            self.__signingCertChain = None   
313             
314        # MUST be set before __setSigningPriKeyFilePath / __setSigningPriKey
315        # are called
316        self.__setSigningPriKeyPwd(self.cfg['signingPriKeyPwd'])
317       
318        if self.cfg.get('signingPriKey'):
319            # Don't allow None for private key setting
320            self.__setSigningPriKey(self.cfg['signingPriKey'])
321           
322        self.__setSigningPriKeyFilePath(self.cfg['signingPriKeyFilePath'])
323       
324        # CA certificate(s) for verification of X.509 certificate used with
325        # signature.
326        if self.cfg.get('caCertDirPath'):
327            self.caCertDirPath = self.cfg['caCertDirPath']
328           
329        elif self.cfg.get('caCertFilePathList'):
330            self.caCertFilePathList = self.cfg['caCertFilePathList']
331           
332        self.addTimestamp = self.cfg['addTimestamp']
333        self.applySignatureConfirmation=self.cfg['applySignatureConfirmation']
334        self.b64EncSignatureValue = None
335       
336        log.debug("WSSE Config = %s" % self.cfg)
337
338               
339    #_________________________________________________________________________
340    def __setReqBinSecTokValType(self, value):
341        """Set ValueType attribute for BinarySecurityToken used in a request
342         
343        @type value: string
344        @param value: name space for BinarySecurityToken ValueType check
345        'binSecTokValType' class variable for supported types.  Input can be
346        shortened to binSecTokValType keyword if desired.
347        """
348       
349        if value in self.__class__.binSecTokValType:
350            self.__reqBinSecTokValType = self.__class__.binSecTokValType[value]
351 
352        elif value in self.__class__.binSecTokValType.values():
353            self.__reqBinSecTokValType = value
354        else:
355            raise WSSecurityError, \
356                'Request BinarySecurityToken ValueType "%s" not recognised' %\
357                value
358           
359       
360    reqBinSecTokValType = property(fset=__setReqBinSecTokValType,
361         doc="ValueType attribute for BinarySecurityToken used in request")
362       
363
364    #_________________________________________________________________________
365    def __checkC14nKw(self, kw):
366        """Check keywords for canonicalization in signing process - generic
367        method for setting keywords for reference element and SignedInfo
368        element c14n
369       
370        @type kw: dict
371        @param kw: keyword used with ZSI.wstools.Utility.Canonicalization"""
372       
373        # Check for dict/None - Set to None in order to use inclusive
374        # canonicalization
375        if kw is not None and not isinstance(kw, dict):
376            # Otherwise keywords must be a dictionary
377            raise AttributeError, \
378                "Expecting dictionary type for reference c14n keywords"
379               
380        elif kw.get('inclusive_namespaces') and \
381             not isinstance(kw['inclusive_namespaces'], list) and \
382             not isinstance(kw['inclusive_namespaces'], tuple):
383            raise AttributeError, \
384                'Expecting list or tuple of prefix names for "%s" keyword' % \
385                'inclusive_namespaces'
386       
387               
388    #_________________________________________________________________________
389    def __setRefC14nKw(self, kw):
390        """Set keywords for canonicalization of reference elements in the
391        signing process"""
392        self.__checkC14nKw(kw)                   
393        self.__refC14nKw = kw
394       
395    refC14nKw = property(fset=__setRefC14nKw,
396                         doc="Keywords for c14n of reference elements")
397       
398               
399    #_________________________________________________________________________
400    def __setSignedInfoC14nKw(self, kw):
401        """Set keywords for canonicalization of SignedInfo element in the
402        signing process"""
403        self.__checkC14nKw(kw)                   
404        self.__signedInfoC14nKw = kw
405       
406    signedInfoC14nKw = property(fset=__setSignedInfoC14nKw,
407                                doc="Keywords for c14n of SignedInfo element")
408
409
410    #_________________________________________________________________________
411    def __refC14nIsExcl(self):
412        return isinstance(self.__refC14nKw, dict) and \
413               isinstance(self.__refC14nKw.get('inclusive_namespaces'), list)
414               
415    refC14nIsExcl = property(fget=__refC14nIsExcl,
416    doc="Return True/False c14n for reference elements set to exclusive type")
417     
418
419    #_________________________________________________________________________
420    def __signedInfoC14nIsExcl(self):
421        return isinstance(self.__signedInfoC14nKw, dict) and \
422        isinstance(self.__signedInfoC14nKw.get('inclusive_namespaces'), list)
423               
424    signedInfoC14nIsExcl = property(fget=__signedInfoC14nIsExcl,
425    doc="Return True/False c14n for SignedInfo element set to exclusive type")
426   
427   
428    #_________________________________________________________________________
429    def __setCert(self, cert):
430        """filter and convert input cert to signing verifying cert set
431        property methods.  For signingCert, set to None if it is not to be
432        included in the SOAP header.  For verifyingCert, set to None if this
433        cert can be expected to be retrieved from the SOAP header of the
434        message to be verified
435       
436        @type: ndg.security.common.X509.X509Cert / M2Crypto.X509.X509 /
437        string or None
438        @param cert: X.509 certificate. 
439       
440        @rtype ndg.security.common.X509.X509Cert
441        @return X.509 certificate object"""
442       
443        if cert is None or isinstance(cert, X509Cert):
444            # ndg.security.common.X509.X509Cert type / None
445            return cert
446           
447        elif isinstance(cert, X509.X509):
448            # M2Crypto.X509.X509 type
449            return X509Cert(m2CryptoX509=cert)
450           
451        elif isinstance(cert, basestring):
452            return X509CertParse(cert)
453       
454        else:
455            raise AttributeError, "X.509 Cert. must be type: " + \
456                "ndg.security.common.X509.X509Cert, M2Crypto.X509.X509 or " +\
457                "a base64 encoded string"
458
459   
460    #_________________________________________________________________________
461    def __getVerifyingCert(self):
462        '''Return X.509 cert object corresponding to cert used to verify the
463        signature in the last call to verify
464       
465         * Cert will correspond to one used in the LATEST call to verify, on
466         the next call it will be replaced
467         * if verify hasn't been called, the cert will be None
468       
469        @rtype: M2Crypto.X509.X509
470        @return: certificate object
471        '''
472        return self.__verifyingCert
473
474
475    #_________________________________________________________________________
476    def __setVerifyingCert(self, verifyingCert):
477        "Set property method for X.509 cert. used to verify a signature"
478        self.__verifyingCert = self.__setCert(verifyingCert)
479   
480        # Reset file path as it may no longer apply
481        self.__verifyingCertFilePath = None
482       
483    verifyingCert = property(fset=__setVerifyingCert,
484                             fget=__getVerifyingCert,
485                             doc="Set X.509 Cert. for verifying signature")
486
487
488    #_________________________________________________________________________
489    def __setVerifyingCertFilePath(self, verifyingCertFilePath):
490        "Set method for Service X.509 cert. file path property"
491       
492        if isinstance(verifyingCertFilePath, basestring):
493            self.__verifyingCert = X509CertRead(verifyingCertFilePath)
494           
495        elif verifyingCertFilePath is not None:
496            raise AttributeError, \
497            "Verifying X.509 Cert. file path must be None or a valid string"
498       
499        self.__verifyingCertFilePath = verifyingCertFilePath
500       
501    verifyingCertFilePath = property(fset=__setVerifyingCertFilePath,
502                    doc="file path of X.509 Cert. for verifying signature")
503
504   
505    #_________________________________________________________________________
506    def __getSigningCert(self):
507        '''Return X.509 cert object corresponding to cert used with
508        signature
509       
510        @rtype: M2Crypto.X509.X509
511        @return: certificate object
512        '''
513        return self.__signingCert
514
515
516    #_________________________________________________________________________
517    def __setSigningCert(self, signingCert):
518        "Set property method for X.509 cert. to be included with signature"
519        self.__signingCert = self.__setCert(signingCert)
520   
521        # Reset file path as it may no longer apply
522        self.__signingCertFilePath = None
523       
524    signingCert = property(fget=__getSigningCert,
525                           fset=__setSigningCert,
526                           doc="X.509 Cert. to include signature")
527
528 
529    #_________________________________________________________________________
530    def __setSigningCertFilePath(self, signingCertFilePath):
531        "Set signature X.509 cert property method"
532       
533        if isinstance(signingCertFilePath, basestring):
534            self.__signingCert = X509CertRead(signingCertFilePath)
535           
536        elif signingCertFilePath is not None:
537            raise AttributeError, \
538                "Signature X.509 cert. file path must be a valid string"
539       
540        self.__signingCertFilePath = signingCertFilePath
541       
542       
543    signingCertFilePath = property(fset=__setSigningCertFilePath,
544                   doc="File path X.509 cert. to include with signed message")
545
546   
547    #_________________________________________________________________________
548    def __setSigningCertChain(self, signingCertChain):
549        '''Signature set-up with "X509PKIPathv1" BinarySecurityToken
550        ValueType.  Use an X.509 Stack to store certificates that make up a
551        chain of trust to certificate used to verify a signature
552       
553        @type signingCertChain: list or tuple of M2Crypto.X509.X509Cert or
554        ndg.security.common.X509.X509Cert types.
555        @param signingCertChain: list of certificate objects making up the
556        chain of trust.  The last certificate is the one associated with the
557        private key used to sign the message.'''
558       
559        if not isinstance(signingCertChain, list) and \
560           not isinstance(signingCertChain, tuple):
561            raise WSSecurityError, \
562                        'Expecting a list or tuple for "signingCertChain"'
563       
564        self.__signingCertChain = X509Stack()
565           
566        for cert in signingCertChain:
567            self.__signingCertChain.push(cert)
568           
569    signingCertChain = property(fset=__setSigningCertChain,
570               doc="Cert.s in chain of trust to cert. used to verify msg.")
571
572 
573    #_________________________________________________________________________
574    def __setSigningPriKeyPwd(self, signingPriKeyPwd):
575        "Set method for private key file password used to sign message"
576        if signingPriKeyPwd is not None and \
577           not isinstance(signingPriKeyPwd, basestring):
578            raise AttributeError, \
579                "Signing private key password must be None or a valid string"
580       
581        self.__signingPriKeyPwd = signingPriKeyPwd
582       
583    signingPriKeyPwd = property(fset=__setSigningPriKeyPwd,
584             doc="Password protecting private key file used to sign message")
585
586 
587    #_________________________________________________________________________
588    def __setSigningPriKey(self, signingPriKey):
589        """Set method for client private key
590       
591        Nb. if input is a string, signingPriKeyPwd will need to be set if
592        the key is password protected.
593       
594        @type signingPriKey: M2Crypto.RSA.RSA / string
595        @param signingPriKey: private key used to sign message"""
596       
597        if isinstance(signingPriKey, basestring):
598            pwdCallback = lambda *ar, **kw: self.__signingPriKeyPwd
599            self.__signingPriKey = RSA.load_key_string(signingPriKey,
600                                                       callback=pwdCallback)
601
602        elif isinstance(signingPriKey, RSA.RSA):
603            self.__signingPriKey = signingPriKey
604                   
605        else:
606            raise AttributeError, "Signing private key must be a valid " + \
607                                  "M2Crypto.RSA.RSA type or a string"
608               
609    signingPriKey = property(fset=__setSigningPriKey,
610                             doc="Private key used to sign outbound message")
611
612 
613    #_________________________________________________________________________
614    def __setSigningPriKeyFilePath(self, signingPriKeyFilePath):
615        """Set method for client private key file path
616       
617        signingPriKeyPwd MUST be set prior to a call to this method"""
618        if isinstance(signingPriKeyFilePath, basestring):                           
619            try:
620                # Read Private key to sign with   
621                priKeyFile = BIO.File(open(signingPriKeyFilePath)) 
622                pwdCallback = lambda *ar, **kw: self.__signingPriKeyPwd                                           
623                self.__signingPriKey = RSA.load_key_bio(priKeyFile, 
624                                                        callback=pwdCallback)           
625            except Exception, e:
626                raise AttributeError, \
627                                "Setting private key for signature: %s" % e
628       
629        elif signingPriKeyFilePath is not None:
630            raise AttributeError, \
631                        "Private key file path must be a valid string or None"
632       
633        self.__signingPriKeyFilePath = signingPriKeyFilePath
634       
635    signingPriKeyFilePath = property(fset=__setSigningPriKeyFilePath,
636                      doc="File path for private key used to sign message")
637
638    def __caCertIsSet(self):
639        '''Check for CA certificate set (X.509 Stack has been created)'''
640        return hasattr(self, '_SignatureHandler__caX509Stack')
641   
642    caCertIsSet = property(fget=__caCertIsSet,
643           doc='Check for CA certificate set (X.509 Stack has been created)')
644   
645    #_________________________________________________________________________
646    def __appendCAX509Stack(self, caCertList):
647        '''Store CA certificates in an X.509 Stack
648       
649        @param caCertList: list or tuple
650        @type caCertList: M2Crypto.X509.X509 certificate objects'''
651       
652        if not self.caCertIsSet:
653            self.__caX509Stack = X509Stack()
654           
655        for cert in caCertList:
656            self.__caX509Stack.push(cert)
657
658
659    #_________________________________________________________________________
660    def __setCAX509StackFromDir(self, caCertDir):
661        '''Read CA certificates from directory and add them to the X.509
662        stack
663       
664        @param caCertDir: string
665        @type caCertDir: directory from which to read CA certificate files'''
666       
667        # Mimic OpenSSL -CApath option which expects directory of CA files
668        # of form <Hash cert subject name>.0
669        reg = re.compile('\d+\.0')
670        try:
671            caCertList = [X509CertRead(caFile) \
672                          for caFile in os.listdir(caCertDir) \
673                          if reg.match(caFile)]
674        except Exception, e:
675            raise WSSecurityError, \
676                'Loading CA certificate "%s" from CA directory: %s' % \
677                                                        (caFile, str(e))
678                   
679        # Add to stack
680        self.__appendCAX509Stack(caCertList)
681       
682    caCertDirPath = property(fset=__setCAX509StackFromDir,
683                      doc="Dir. containing CA cert.s used for verification")
684
685
686    #_________________________________________________________________________
687    def __setCAX509StackFromCertFileList(self, caCertFilePathList):
688        '''Read CA certificates from file and add them to the X.509
689        stack
690       
691        @type caCertFilePathList: list or tuple
692        @param caCertFilePathList: list of file paths for CA certificates to
693        be used to verify certificate used to sign message'''
694       
695        if not isinstance(caCertFilePathList, list) and \
696           not isinstance(caCertFilePathList, tuple):
697            raise WSSecurityError, \
698                        'Expecting a list or tuple for "caCertFilePathList"'
699
700        # Mimic OpenSSL -CApath option which expects directory of CA files
701        # of form <Hash cert subject name>.0
702        try:
703            caCertList = [X509CertRead(caFile) \
704                          for caFile in caCertFilePathList]
705        except Exception, e:
706            raise WSSecurityError, \
707                    'Loading CA certificate "%s" from file list: %s' % \
708                                                        (caFile, str(e))
709                   
710        # Add to stack
711        self.__appendCAX509Stack(caCertList)
712       
713    caCertFilePathList = property(fset=__setCAX509StackFromCertFileList,
714                      doc="List of CA cert. files used for verification")
715               
716
717    def _applySignatureConfirmation(self, wsseElem):
718        '''Add SignatureConfirmation element - as specified in WS-Security 1.1
719        - to outbound message on receipt of a signed message from a client
720       
721        This has been added in through tests vs. Apache Axis Rampart client
722       
723        @type wsseElem:
724        @param wsseElem: wsse:Security element'''
725        if self.b64EncSignatureValue is None:
726            log.info(\
727"SignatureConfirmation element requested but no request signature was cached")
728            return
729       
730        sigConfirmElem = ElementTree.Element("{%s}%s" % (OASIS.WSSE11, 
731                                                      'SignatureConfirmation'))
732        wsseElem.append(sigConfirmElem)
733       
734        # Add ID so that the element can be included in the signature
735        sigConfirmElem.set('{%s}Id' % WSU.UTILITY, "signatureConfirmation")
736
737        # Add ID so that the element can be included in the signature
738        # Following line is a hack to avoid appearance of #x when serialising
739        # \n chars
740        # TODO: Fix #x problem with sig value?
741        b64EncSignatureValue = ''.join(self.b64EncSignatureValue.split('\n'))
742        sigConfirmElem.set('Value', b64EncSignatureValue)
743       
744       
745    def _addTimeStamp(self, wsseElem, elapsedSec=60*5):
746        '''Add a timestamp to wsse:Security section of message to be signed
747        e.g.
748            <wsu:Timestamp wsu:Id="timestamp">
749               <wsu:Created>2008-03-25T14:40:37.319Z</wsu:Created>
750               <wsu:Expires>2008-03-25T14:45:37.319Z</wsu:Expires>
751            </wsu:Timestamp>
752       
753        @type wsseElem:
754        @param wsseElem: wsse:Security element
755        @type elapsedSec: int   
756        @param elapsedSec: time interval in seconds between Created and Expires
757        time stamp values
758        '''
759        # Nb. wsu ns declaration is in the SOAP header elem
760        timestampElem = ElementTree.Element("{%s}%s"%(WSU.UTILITY,'Timestamp'))
761        wsseElem.append(timestampElem)
762       
763        # Add ID so that the timestamp element can be included in the signature
764        timestampElem.set('{%s}Id' % WSU.UTILITY, "timestamp")
765       
766        # Value type can be any be any one of those supported via
767        # binSecTokValType
768        createdElem = ElementTree.Element("{%s}%s" % (WSU.UTILITY,'Created'))
769        timestampElem.append(createdElem)
770       
771        dtCreatedTime = datetime.utcnow()
772        createdElem.text = dtCreatedTime.isoformat('T') + 'Z'
773       
774        dtExpiryTime = dtCreatedTime + timedelta(seconds=elapsedSec)
775        expiresElem = ElementTree.Element("{%s}%s" % (WSU.UTILITY, 'Expires'))
776        timestampElem.append(expiresElem)
777       
778        expiresElem.text = dtExpiryTime.isoformat('T') + 'Z'
779       
780
781    def _verifyTimeStamp(self, parsedSOAP, timestampMustBeSet=False):
782        """Call from verify to check timestamp if found. 
783       
784        TODO: refactor input args - maybe these should by object attributes
785       
786        @type parsedSOAP: ZSI.parse.ParsedSoap
787        @param parsedSOAP: object contain parsed SOAP message received from
788        sender
789        @type timestampMustBeSet: bool
790        @param timestampMustBeSet: set to True to raise an exception if no
791        timestamp element is present in the message to be checked"""
792
793        timestampElems = self._soapEnvElem.findall('.//wsu:Timestamp', 
794                                                namespaces=self._processorNSs)
795        nTimestampElems = len(timestampElems)       
796        if nTimestampElems > 1:
797            raise TimestampError("Expecting only one timestamp element")
798       
799        elif nTimestampElems == 0:
800            msg = "Verifying message - No timestamp element found"
801            if timestampMustBeSet:
802                raise VerifyError(msg)
803            else:
804                log.warning(msg)
805                return
806       
807        timestampElem = timestampElems[0]
808       
809        # Time now
810        dtNow = datetime.utcnow()
811       
812        createdElem = timestampElem.find("wsu:Created", 
813                                         namespaces=self._processorNSs)
814        if createdElem is None:
815            raise TimestampError("No 'Created' element found in timestamp")
816
817        # Workaround for fractions of second with datetime module
818        try:
819            createdDateTime,strCreatedSecFraction = createdElem.text.split('.')
820            strCreatedSecFraction = strCreatedSecFraction.split('Z')[0]
821            createdExp = -int(len(strCreatedSecFraction))
822            createdSecFraction = int(strCreatedSecFraction) * 10 ** createdExp
823           
824        except ValueError, e:
825            raise ValueError("Parsing timestamp Created element: %s" % e)
826       
827
828        dtCreated = datetime.strptime(createdDateTime, '%Y-%m-%dT%H:%M:%S')
829        dtCreated += timedelta(seconds=createdSecFraction)
830        if dtCreated >= dtNow:
831            raise TimestampError(\
832        "Timestamp created time %s is equal to or after the current time %s" %\
833                (dtCreated, dtNow))
834       
835        expiresElem = timestampElem.find("wsu:Expires",
836                                         namespaces=self._processorNSs)
837        if expiresElem is None:
838            raise TimestampError("Verifying message - No Expires element "
839                                 "found in Timestamp")
840
841        try:
842            expiryDateTime, strExpirySecFraction = expiresElem.text.split('.')
843            strExpirySecFraction = strExpirySecFraction.split('Z')[0]
844            expiryExp = -int(len(strExpirySecFraction))
845            expirySecFraction = int(strExpirySecFraction) * 10 ** expiryExp
846        except ValueError, e:
847            raise ValueError("Parsing timestamp Expires element: %s" % e)
848       
849        dtExpiry = datetime.strptime(expiryDateTime, '%Y-%m-%dT%H:%M:%S')
850        dtExpiry += timedelta(seconds=expirySecFraction)
851        if dtExpiry < dtNow:
852            raise TimestampError(\
853                "Timestamp expiry time %s is before the current time %s" % \
854                (dtCreated, dtNow))
855           
856        log.debug("Verified timestamp")
857           
858                   
859    #_________________________________________________________________________
860    def sign(self, soapWriter):
861        '''Sign the message body and binary security token of a SOAP message
862       
863        @type soapWriter: ZSI.writer.SoapWriter
864        @param soapWriter: ZSI object to write SOAP message
865        '''
866       
867        # Check for expected soapWriter DOM class
868        if not isinstance(soapWriter.dom, ElementTreeProxy):
869            raise SignatureError("Expecting ElementTreeProxy type for "
870                                 "ZSI.writer.SoapWriter.dom instance")
871           
872        # Add X.509 cert as binary security token
873        # TODO: sub encodestring with b64encode?
874        if self.__reqBinSecTokValType==self.binSecTokValType['X509PKIPathv1']:
875            binSecTokVal = base64.encodestring(self.__signingCertChain.asDER())
876        else:
877            # Assume X.509 / X.509 vers 3
878            binSecTokVal = base64.encodestring(self.__signingCert.asDER())
879
880        self._soapEnvElem = soapWriter.dom._elem
881        soapHdrElem = soapWriter._header._elem
882        soapBodyElem = soapWriter.body._elem
883       
884        self._soapEnvElem.set("xmlns:%s" % 'ds', DSIG.BASE)
885        #self._soapEnvElem.set("xmlns:%s" % 'wsse', OASIS.WSSE)
886       
887        soapHdrElem.set("xmlns:%s" % 'wsse', OASIS.WSSE)
888        soapHdrElem.set("xmlns:%s" % 'wsse11', OASIS.WSSE11)
889#        soapHdrElem.set("xmlns:%s" % 'wsu', WSU.UTILITY)
890        self._soapEnvElem.set("xmlns:%s" % 'wsu', WSU.UTILITY)
891        soapHdrElem.set("xmlns:%s" % 'ds', DSIG.BASE)
892       
893        try:
894            refC14nPfxSet = len(self.__refC14nKw['inclusive_namespaces']) > 0
895        except KeyError:
896            refC14nPfxSet = False
897
898        try:
899            signedInfoC14nPfxSet = \
900                len(self.__signedInfoC14nKw['inclusive_namespaces']) > 0
901        except KeyError:
902            signedInfoC14nPfxSet = False
903               
904        if refC14nPfxSet:
905            soapHdrElem.set("xmlns:%s" % 'ec', DSIG.C14N_EXCL)
906           
907           
908        # Check <wsse:security> isn't already present in header
909        wsseElems = self._soapEnvElem.findall('.//wsse:security',
910                                              namespaces=self._processorNSs)
911        if len(wsseElems) > 1:
912            raise SignatureError('wsse:Security element is already present')
913       
914        # Add WSSE element
915        wsseElem = ElementTree.Element("{%s}%s" % (OASIS.WSSE, 'Security'))
916        soapHdrElem.append(wsseElem)
917       
918        wsseElem.set("xmlns:%s" % 'wsse', OASIS.WSSE)
919       
920        # Recipient MUST parse and check this signature
921        wsseElem.set('SOAP-ENV:mustUnderstand', "1")
922       
923        # Binary Security Token element will contain the X.509 cert
924        # corresponding to the private key used to sing the message
925        binSecTokElem = ElementTree.Element("{%s}%s" % (OASIS.WSSE, 
926                                                        'BinarySecurityToken'))
927        wsseElem.append(binSecTokElem)
928       
929        # Value type can be any be any one of those supported via
930        # binSecTokValType
931        binSecTokElem.set('ValueType', self.__reqBinSecTokValType)
932        binSecTokElem.set('EncodingType', SignatureHandler.binSecTokEncType)
933       
934        # Add ID so that the binary token can be included in the signature
935        binSecTokElem.set('{%s}Id' % WSU.UTILITY, "binaryToken")
936       
937        binSecTokElem.text = binSecTokVal
938
939
940        # Timestamp
941        if self.addTimestamp:
942            self._addTimeStamp(wsseElem)
943           
944        # Signature Confirmation
945        if self.applySignatureConfirmation: 
946            self._applySignatureConfirmation(wsseElem)
947       
948        # Signature
949        signatureElem = ElementTree.Element("{%s}%s" % (DSIG.BASE,'Signature'))
950        wsseElem.append(signatureElem)
951       
952        # Signature - Signed Info
953        signedInfoElem = ElementTree.Element("{%s}%s"%(DSIG.BASE,'SignedInfo'))
954        signatureElem.append(signedInfoElem)
955       
956        # Signed Info - Canonicalization method
957        c14nMethodElem = ElementTree.Element("{%s}%s" % (DSIG.BASE,
958                                                    'CanonicalizationMethod'))
959        signedInfoElem.append(c14nMethodElem)
960       
961        # Set based on 'signedInfoIsExcl' property
962        c14nAlgOpt = (DSIG.C14N, DSIG.C14N_EXCL)
963        signedInfoC14nAlg = c14nAlgOpt[int(self.signedInfoC14nIsExcl)]
964       
965        c14nMethodElem.set('Algorithm', signedInfoC14nAlg)
966       
967        if signedInfoC14nPfxSet:
968            c14nInclNamespacesElem = ElementTree.Element("{%s}%s" % \
969                                                    (signedInfoC14nAlg,
970                                                    'InclusiveNamespaces'))
971            c14nMethodElem.append(c14nInclNamespacesElem)
972           
973            pfxList = ' '.join(self.__signedInfoC14nKw['inclusive_namespaces'])
974            c14nInclNamespacesElem.set('PrefixList', pfxList)
975
976       
977        # Signed Info - Signature method
978        sigMethodElem = ElementTree.Element("{%s}%s" % \
979                                            (DSIG.BASE, 'SignatureMethod'))
980        signedInfoElem.append(sigMethodElem)
981        sigMethodElem.set('Algorithm', DSIG.SIG_RSA_SHA1)
982       
983        # Signature - Signature value
984        signatureValueElem = ElementTree.Element("{%s}%s" % (DSIG.BASE, 
985                                                             'SignatureValue'))
986        signatureElem.append(signatureValueElem)
987       
988        # Key Info
989        KeyInfoElem = ElementTree.Element("{%s}%s" % (DSIG.BASE, 'KeyInfo'))
990        signatureElem.append(KeyInfoElem)
991       
992        secTokRefElem = ElementTree.Element("{%s}%s" % (OASIS.WSSE, 
993                                                    'SecurityTokenReference'))
994        KeyInfoElem.append(secTokRefElem)
995       
996        # Reference back to the binary token included earlier
997        wsseRefElem = ElementTree.Element("{%s}%s" % (OASIS.WSSE, 'Reference'))
998        secTokRefElem.append(wsseRefElem)
999       
1000        wsseRefElem.set('URI', "#binaryToken")
1001       
1002        # Add Reference to body so that it can be included in the signature
1003        #soapBodyElem.set('xmlns:wsu', WSU.UTILITY) - results in duplicate xmlns declarations
1004        soapBodyElem.set('{%s}Id' % WSU.UTILITY, 'body')
1005
1006
1007        # Set Reference Canonicalization algorithm based on 'refC14nIsExcl'
1008        # property
1009        refC14nAlg = c14nAlgOpt[self.refC14nIsExcl]
1010       
1011        # Pick up all the wsu:Id tagged elements set in the above
1012        refElems = self._soapEnvElem.findall('.//*[@wsu:Id]',
1013                                             namespaces=self._processorNSs)
1014
1015        # 1) Reference Generation
1016        #
1017        # Find references
1018        for refElem in refElems:
1019           
1020            # Set URI attribute to point to reference to be signed
1021            uri = '#' + refElem.get('{%s}%s' % (WSU.UTILITY, 'Id'))
1022           
1023            # Canonicalize reference
1024#            refC14n = self.canonicalize(subset=refElem,
1025#                                        exclusive=self.refC14nIsExcl,
1026#                                        **self.__refC14nKw)
1027            refC14n = soapWriter.dom.canonicalize(subset=refElem,
1028                                                  exclusive=self.refC14nIsExcl,
1029                                                  **self.__refC14nKw)
1030            log.debug('Canonicalisation for URI "%s": %s', uri, refC14n)
1031           
1032            # Calculate digest for reference and base 64 encode
1033            #
1034            # Nb. encodestring adds a trailing newline char
1035            # Use b64encode instead - encodestring puts in newline chars at
1036            # 76 char intervals
1037            #digestValue = base64.encodestring(sha(refC14n).digest()).strip()
1038            digestValue = base64.b64encode(sha(refC14n).digest())
1039           
1040            # Add a new reference element to SignedInfo
1041            signedInfoRefElem = ElementTree.Element("{%s}Reference"%DSIG.BASE)
1042            signedInfoElem.append(signedInfoRefElem)
1043            signedInfoRefElem.set('URI', uri)
1044           
1045            # Use ds:Transforms or wsse:TransformationParameters?
1046            transformsElem = ElementTree.Element("{%s}Transforms" % DSIG.BASE)
1047            signedInfoRefElem.append(transformsElem)
1048           
1049            transformElem = ElementTree.Element("{%s}Transform" % DSIG.BASE)
1050            transformsElem.append(transformElem)
1051
1052            # Set Canonicalization algorithm type
1053            transformElem.set('Algorithm', refC14nAlg)
1054            if refC14nPfxSet:
1055                # Exclusive C14N requires inclusive namespace elements
1056                inclNamespacesElem = transformElem.createAppendElement(\
1057                                                       refC14nAlg,
1058                                                       'InclusiveNamespaces')
1059                refPfxList = ' '.join(self.__refC14nKw['inclusive_namespaces'])
1060                inclNamespacesElem.set('PrefixList', refPfxList)
1061           
1062            # Digest Method
1063            digestMethodElem = ElementTree.Element("{%s}%s" % (DSIG.BASE, 
1064                                                               'DigestMethod'))
1065            signedInfoRefElem.append(digestMethodElem)
1066           
1067            digestMethodElem.set('Algorithm', DSIG.DIGEST_SHA1)
1068           
1069            # Digest Value
1070            digestValueElem = ElementTree.Element("{%s}%s" % (DSIG.BASE, 
1071                                                              'DigestValue'))
1072            signedInfoRefElem.append(digestValueElem)
1073            digestValueElem.text = digestValue
1074   
1075        # 2) Signature Generation
1076        #       
1077        # Canonicalize the signedInfo node
1078        c14nSignedInfo = soapWriter.dom.canonicalize(subset=signedInfoElem,
1079                                           exclusive=self.signedInfoC14nIsExcl,
1080                                           **self.__signedInfoC14nKw)
1081        log.debug('Canonicalisation for <ds:signedInfo>: %s', c14nSignedInfo)
1082       
1083        # Calculate digest of SignedInfo
1084        signedInfoDigestValue = sha(c14nSignedInfo).digest()
1085       
1086        # Sign using the private key and base 64 encode the result
1087        signatureValue = self.__signingPriKey.sign(signedInfoDigestValue)
1088       
1089        # encodestring puts newline markers at 76 char intervals otherwise no
1090        # difference
1091        # b64EncSignatureValue = base64.encodestring(signatureValue).strip()
1092        b64EncSignatureValue = base64.b64encode(signatureValue)
1093
1094        # Add to <SignatureValue>
1095        signatureValueElem.text = b64EncSignatureValue
1096        log.debug("Signature generation complete")
1097
1098
1099    def verify(self, parsedSOAP):
1100        """Verify signature
1101       
1102        @type parsedSOAP: ZSI.parse.ParsedSoap
1103        @param parsedSOAP: object contain parsed SOAP message received from
1104        sender"""
1105
1106        if not isinstance(parsedSOAP.dom, ElementTreeProxy):
1107            raise VerifyError("Expecting ElementTreeProxy type for "
1108                              "ZSI.parse.ParsedSoap.dom")
1109       
1110        self._soapEnvElem = parsedSOAP.dom._elem
1111
1112        signatureElems = self._soapEnvElem.findall('.//ds:Signature', 
1113                                                namespaces=self._processorNSs)       
1114        if len(signatureElems) > 1:
1115            raise VerifyError('Multiple <ds:Signature/> elements found')
1116       
1117        try:
1118            signatureElems = signatureElems[0]
1119        except:
1120            # Message wasn't signed - may be possible if peer raised a SOAP
1121            # fault
1122            raise NoSignatureFound("Input message wasn't signed!")
1123
1124       
1125        # Two stage process: reference validation followed by signature
1126        # validation
1127       
1128        # 1) Reference Validation
1129       
1130        # Check for canonicalization set via ds:CanonicalizationMethod -
1131        # Use this later as a back up in case no Canonicalization was set in
1132        # the transforms elements
1133        c14nMethodElem = self._soapEnvElem.find('.//ds:CanonicalizationMethod', 
1134                                                namespaces=self._processorNSs)
1135        if c14nMethodElem is None:
1136            raise VerifyError("No <ds:Canonicalization/> element found") 
1137             
1138        refElems = self._soapEnvElem.findall('.//ds:Reference', 
1139                                             namespaces=self._processorNSs)
1140        if len(refElems) == 0:
1141            raise VerifyError("No <ds:Reference/> elements found") 
1142       
1143        for refElem in refElems:
1144            # Get the URI for the reference
1145            refURI = refElem.get('URI')
1146                         
1147            transformElem = refElem.find('ds:Transforms/ds:Transform',
1148                                         namespaces=self._processorNSs)
1149            if transformElem is None:
1150                raise VerifyError('Failed to get transform algorithm for '
1151                                  '<ds:Reference URI="%s">' % refURI)
1152               
1153            refAlgorithm = transformElem.get("Algorithm")
1154               
1155            # Add extra keyword for Exclusive canonicalization method
1156            refC14nKw = dict(exclusive=refAlgorithm == DSIG.C14N_EXCL)
1157            if refC14nKw['exclusive']:
1158                # Check for no inclusive namespaces set
1159                inclusiveNSElem = transformElem.find("InclusiveNamespaces",
1160                                                namespaces=self._processorNSs)                   
1161                if inclusiveNSElem is not None:
1162                    pfxListTxt = inclusiveNSElem.get('PrefixList')
1163                    if pfxListTxt is None:
1164                        raise VerifyError('Empty InclusiveNamespaces list for'\
1165                                          ' <ds:Reference URI="%s">' % refURI)
1166                                                 
1167                    refC14nKw['inclusive_namespaces'] = pfxListTxt.split()
1168                else:
1169                    # Set to empty list to ensure Exclusive C14N is set for
1170                    # Canonicalize call
1171                    refC14nKw['inclusive_namespaces'] = []
1172       
1173            # Canonicalize the reference data and calculate the digest
1174            if refURI[0] != "#":
1175                raise VerifyError('Expecting # identifier for Reference URI' \
1176                                  ' "%s"' % refURI)
1177                   
1178            # XPath reference
1179            uriXPath = './/*[@wsu:Id="%s"]' % refURI[1:]
1180            uriElem=self._soapEnvElem.findall(uriXPath,
1181                                              namespaces=self._processorNSs)
1182            if len(uriElem) > 1:
1183                raise VerifyError("Multiple elements matching '%s' search" 
1184                                  " path: %s" % (uriXPath, uriElem))
1185
1186            refC14n=parsedSOAP.dom.canonicalize(subset=uriElem[0], **refC14nKw)
1187           
1188            # encodestring adds line delimiters at 76 char intervals - avoid
1189            # and use b64encode instead           
1190            calculatedDigestValue = base64.b64encode(sha(refC14n).digest())
1191           
1192            # Extract the digest value that was stored in the SOAP request         
1193            digestElem = refElem.find('ds:DigestValue',
1194                                      namespaces=self._processorNSs)
1195            if digestElem is None:
1196                raise VerifyError('Failed to get digestValue for ' \
1197                                  '<ds:Reference URI="%s">' % refURI)
1198               
1199            # Need to check here for value split into separate lines?
1200            retrievedDigestValue = str(digestElem.text).strip()   
1201           
1202            # Reference validates if the two digest values are the same
1203            if retrievedDigestValue != calculatedDigestValue:
1204                log.error("Digest values don't match")
1205                log.error('Canonicalisation for URI: "%s": %s' % \
1206                          (refURI, refC14n))
1207                raise InvalidSignature('Digest Values do not match for URI:' \
1208                                       ' "%s"' % refURI)
1209           
1210            log.debug("Verified canonicalization for element '%s'"%refURI[1:])
1211           
1212
1213        # 2) Signature Validation
1214        signedInfoElem = self._soapEnvElem.find('.//ds:SignedInfo',
1215                                                namespaces=self._processorNSs)
1216        if signedInfoElem is None:
1217            raise VerifyError("No <ds:signedInfo/> section found")
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 = c14nMethodElem.get("Algorithm")
1223        if signedInfoC14nAlg is None:
1224            raise VerifyError('No Algorithm attribute set for <signedInfo/>' \
1225                              ' section')
1226           
1227        signedInfoC14nKw = dict(exclusive=signedInfoC14nAlg == DSIG.C14N_EXCL)
1228        if signedInfoC14nKw['exclusive']:
1229
1230            # Check for no inclusive namespaces set
1231            inclusiveNSElem = c14nMethodElem.find("InclusiveNamespaces",
1232                                                namespaces=self._processorNSs)                   
1233            if inclusiveNSElem is not None:
1234                pfxListTxt = inclusiveNSElem.get('PrefixList')
1235                if pfxListTxt is None:
1236                    raise VerifyError('Empty InclusiveNamespaces list for'\
1237                                      ' <ds:Reference URI="%s">' % refURI)
1238                                             
1239                signedInfoC14nKw['inclusive_namespaces'] = pfxListTxt.split()
1240            else:
1241                # Set to empty list to ensure Exclusive C14N is set for
1242                # Canonicalize call
1243                signedInfoC14nKw['inclusive_namespaces'] = []
1244
1245        # Canonicalize the SignedInfo node and take digest
1246        c14nSignedInfo = parsedSOAP.dom.canonicalize(subset=signedInfoElem, 
1247                                                     **signedInfoC14nKw)                     
1248        signedInfoDigestValue = sha(c14nSignedInfo).digest()
1249       
1250        # Get the signature value in order to check against the digest just
1251        # calculated
1252        signatureValueElem = self._soapEnvElem.find('.//ds:SignatureValue',
1253                                                namespaces=self._processorNSs)
1254
1255        # Remove base 64 encoding
1256        b64EncSignatureValue = signatureValueElem.text
1257        signatureValue = base64.decodestring(b64EncSignatureValue)
1258
1259        # Cache Signature Value here so that a response can include it
1260        if self.applySignatureConfirmation:
1261            # re-encode string to avoid possible problems with interpretation
1262            # of line breaks
1263            self.b64EncSignatureValue = b64EncSignatureValue
1264        else:
1265            self.b64EncSignatureValue = None
1266         
1267        # Look for X.509 Cert in wsse:BinarySecurityToken element -
1268        # Signature may not have included the Binary Security Token in
1269        # which case the verifying cert will need to have been set
1270        # elsewhere
1271        binSecTokElem = self._soapEnvElem.find('.//wsse:BinarySecurityToken',
1272                                               namespaces=self._processorNSs)       
1273        if binSecTokElem is not None:
1274            x509CertTxt = str(binSecTokElem.text)
1275           
1276            valueType = binSecTokElem.get("ValueType")
1277            if valueType in (SignatureHandler.binSecTokValType['X509v3'],
1278                             SignatureHandler.binSecTokValType['X509']):
1279                # Remove base 64 encoding
1280                derString = base64.decodestring(x509CertTxt)
1281               
1282                # Load from DER format into M2Crypto.X509
1283                m2X509Cert = X509.load_cert_string(derString,
1284                                                   format=X509.FORMAT_DER)
1285                self.__setVerifyingCert(m2X509Cert)
1286               
1287                x509Stack = X509Stack()
1288
1289            elif valueType == \
1290                SignatureHandler.binSecTokValType['X509PKIPathv1']:
1291               
1292                derString = base64.decodestring(x509CertTxt)
1293                x509Stack = X509StackParseFromDER(derString)
1294               
1295                # TODO: Check ordering - is the last off the stack the
1296                # one to use to verify the message?
1297                self.__verifyingCert = x509Stack[-1]
1298            else:
1299                raise WSSecurityError('BinarySecurityToken ValueType ' \
1300                    'attribute is not recognised: "%s"' % valueType)
1301
1302        if self.__verifyingCert is None:
1303            raise VerifyError("No certificate set for verification of the " \
1304                              "signature")
1305       
1306        # Extract RSA public key from the cert
1307        rsaPubKey = self.__verifyingCert.pubKey.get_rsa()
1308
1309        # Apply the signature verification
1310        try:
1311            verify = rsaPubKey.verify(signedInfoDigestValue, signatureValue)
1312        except RSA.RSAError, e:
1313            raise VerifyError("Error in Signature: " + str(e))
1314       
1315        if not verify:
1316            raise InvalidSignature("Invalid signature")
1317       
1318        log.debug("Verified signature")
1319       
1320        # Verify chain of trust
1321        x509Stack.verifyCertChain(x509Cert2Verify=self.__verifyingCert,
1322                                  caX509Stack=self.__caX509Stack)
1323
1324        log.debug("Verified certificate chain of trust")
1325       
1326        self._verifyTimeStamp(parsedSOAP) 
1327
1328        log.info("Signature OK")       
1329
1330
1331    def canonicalize(self, **kw):
1332        '''ElementTree based Canonicalization - See ElementC14N for keyword
1333        info'''
1334        f = StringIO()
1335        ElementC14N.write(ElementC14N.build_scoped_tree(self._soapEnvElem), 
1336                          f, 
1337                          **kw)
1338        return f.getvalue()
Note: See TracBrowser for help on using the repository browser.