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

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

wsSecurity.SignatureHandler? - extra options for passing config.

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