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

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