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

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

Working version with independent Policy Enforcement Point (Gatekeeper) + Polciy Decision Point for Pylons Browse code stack

python/ndg.security.server/ndg/security/server/sso/sso/controllers/login.py: extra help info in message for login error

python/ndg.security.test/ndg/security/test/wsSecurity/server/echoServer.py: mod to SignatureHandler? init due to change in WSSecurityConfig interface

python/Tests/authtest/authtest/controllers/test2.py,
python/Tests/authtest/authtest/lib/template.py: missed out on last check in

python/ndg.security.common/ndg/security/common/authz/pdp/proftp.py: udpate to init following change to PDPInterface class for browse code

python/ndg.security.common/ndg/security/common/authz/pdp/init.py: PDPInterface takes cfg keyword which can be file path or a ConfigParser? object

python/ndg.security.common/ndg/security/common/authz/pdp/browse.py:

  • fixes to XPath queries.
  • BrowsePDP now does some more of the work done previously by ows_server.models.ndgInterface.GateKeep and queries directly for role and AA values direct from the doc root
  • made fix to WS-Security settings - may be picked up from the same config file as the PDP settings

python/ndg.security.common/ndg/security/common/authz/pep.py,
python/ndg.security.common/ndg/security/common/wsSecurity.py: allow generic cfg keyword for file path / config obj input

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