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

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

Allow BinarySecurityToken? ValueType? and whether SignatureConfirmation?
is required to be set from server xml properties file.

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