source: TI12-security/trunk/WSSecurity/ndg/wssecurity/common/signaturehandler/etree.py @ 6069

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

Re-release as rc1

Line 
1"""WS-Security digital signature handler for ElementTree XML package
2
3NERC DataGrid Project
4"""
5__author__ = "P J Kershaw"
6__date__ = "02/07/08"
7__copyright__ = "(C) 2009 Science and Technology Facilities Council"
8__license__ = "BSD - see LICENSE file in top-level directory"
9__contact__ = "Philip.Kershaw@stfc.ac.uk"
10__revision__ = '$Id: $'
11import base64
12import os
13import re
14
15# Digest and signature/verify
16from sha import sha
17from M2Crypto import X509, BIO, RSA
18from StringIO import StringIO
19
20from ZSI.wstools.Namespaces import DSIG, SOAP
21
22from elementtree import ElementTree, ElementC14N
23
24# Check SoapWriter.dom attribute is ElementTreeProxy type
25from ndg.security.common.zsi.elementtreeproxy import ElementTreeProxy
26
27from ndg.security.common.wssecurity import WSSecurityError
28from ndg.security.common.wssecurity.signaturehandler import _WSU, OASIS, \
29    BaseSignatureHandler, NoSignatureFound, \
30    InvalidSignature, TimestampError, MessageExpired, VerifyError, \
31    SignatureError
32
33
34from ndg.security.common.X509 import X509Cert, X509CertParse, X509CertRead, \
35X509Stack, X509StackParseFromDER
36
37from datetime import datetime, timedelta
38
39# Workaround for lack of datetime.strptime in Python < 2.5
40if hasattr(datetime, 'strptime'):
41    _strptime = datetime.strptime
42else:
43    from time import strptime
44    _strptime = lambda datetimeStr, format: datetime(*(strptime(datetimeStr, 
45                                                                format)[0:6]))
46import logging
47log = logging.getLogger(__name__)
48
49class SignatureHandler(BaseSignatureHandler):
50    """Class to handle signature and verification of signature with
51    WS-Security
52    """
53
54    # Namespaces for XPath searches
55    _processorNSs = \
56    {
57        'ds':       DSIG.BASE, 
58        'wsu':      _WSU.UTILITY, 
59        'wsse':     OASIS.WSSE, 
60        'SOAP-ENV': SOAP.ENV
61    }
62
63
64    def _applySignatureConfirmation(self, wsseElem):
65        '''Add SignatureConfirmation element - as specified in WS-Security 1.1
66        - to outbound message on receipt of a signed message from a client
67       
68        This has been added in through tests vs. Apache Axis Rampart client
69       
70        @type wsseElem:
71        @param wsseElem: wsse:Security element'''
72        if self.b64EncSignatureValue is None:
73            log.info("SignatureConfirmation element requested but no request "
74                     "signature was cached")
75            return
76       
77        sigConfirmElem = ElementTree.Element("{%s}%s" % (OASIS.WSSE11, 
78                                                      'SignatureConfirmation'))
79        wsseElem.append(sigConfirmElem)
80       
81        # Add ID so that the element can be included in the signature
82        sigConfirmElem.set('{%s}Id' % _WSU.UTILITY, "signatureConfirmation")
83
84        # Add ID so that the element can be included in the signature
85        # Following line is a hack to avoid appearance of #x when serialising
86        # \n chars
87        # TODO: Fix #x problem with sig value?
88        b64EncSignatureValue = ''.join(self.b64EncSignatureValue.split('\n'))
89        sigConfirmElem.set('Value', b64EncSignatureValue)
90       
91       
92    def _addTimeStamp(self, wsseElem, elapsedSec=60*5):
93        '''Add a timestamp to wsse:Security section of message to be signed
94        e.g.
95            <wsu:Timestamp wsu:Id="timestamp">
96               <wsu:Created>2008-03-25T14:40:37.319Z</wsu:Created>
97               <wsu:Expires>2008-03-25T14:45:37.319Z</wsu:Expires>
98            </wsu:Timestamp>
99       
100        @type wsseElem:
101        @param wsseElem: wsse:Security element
102        @type elapsedSec: int   
103        @param elapsedSec: time interval in seconds between Created and Expires
104        time stamp values
105        '''
106        # Nb. wsu ns declaration is in the SOAP header elem
107        timestampElem=ElementTree.Element("{%s}%s"%(_WSU.UTILITY,'Timestamp'))
108        wsseElem.append(timestampElem)
109       
110        # Add ID so that the timestamp element can be included in the signature
111        timestampElem.set('{%s}Id' % _WSU.UTILITY, "timestamp")
112       
113        # Value type can be any be any one of those supported via
114        # binSecTokValType
115        createdElem = ElementTree.Element("{%s}%s" % (_WSU.UTILITY,'Created'))
116        timestampElem.append(createdElem)
117       
118        dtCreatedTime = datetime.utcnow()
119        createdElem.text = dtCreatedTime.isoformat('T') + 'Z'
120       
121        dtExpiryTime = dtCreatedTime + timedelta(seconds=elapsedSec)
122        expiresElem = ElementTree.Element("{%s}%s" % (_WSU.UTILITY, 'Expires'))
123        timestampElem.append(expiresElem)
124       
125        expiresElem.text = dtExpiryTime.isoformat('T') + 'Z'
126       
127
128    def _verifyTimeStamp(self, 
129                         parsedSOAP, 
130                         timestampMustBeSet=False,
131                         createdElemMustBeSet=True,
132                         expiresElemMustBeSet=True):
133        """Call from verify to check timestamp if found. 
134       
135        TODO: refactor input args - maybe these should by object attributes
136       
137        @type parsedSOAP: ZSI.parse.ParsedSoap
138        @param parsedSOAP: object contain parsed SOAP message received from
139        sender
140        @type timestampMustBeSet: bool
141        @type timestampMustBeSet: bool
142        @param timestampMustBeSet: if set to True, raise an exception if no
143        timestamp element is found
144        @type createdElemMustBeSet: bool
145        @param createdElemMustBeSet: if True. raise an exception if no
146        <wsu:Created/> element is present
147        @param expiresElemMustBeSet: if True. raise an exception if no
148        <wsu:Expires/> element is present
149        """
150        timestampElems = self._soapEnvElem.findall('.//wsu:Timestamp', 
151                                                namespaces=self._processorNSs)
152        nTimestampElems = len(timestampElems)       
153        if nTimestampElems > 1:
154            raise TimestampError("Expecting only one timestamp element")
155       
156        elif nTimestampElems == 0:
157            msg = "Verifying message - No timestamp element found"
158            if timestampMustBeSet:
159                raise VerifyError(msg)
160            else:
161                log.warning(msg)
162                return
163       
164        timestampElem = timestampElems[0]
165       
166        # Time now
167        dtNow = datetime.utcnow()
168       
169        createdElem = timestampElem.find("wsu:Created", 
170                                         namespaces=self._processorNSs)
171        if createdElem is None:
172            "No <wsu:Created/> element found in timestamp"
173            if createdElemMustBeSet:
174                raise TimestampError(msg)
175            else:
176                log.warning(msg)
177        else:
178            # Workaround for fractions of second with datetime module
179            try:
180                createdDateTime, strCreatedSecFraction = \
181                                                    createdElem.text.split('.')
182                strCreatedSecFraction = strCreatedSecFraction.split('Z')[0]
183                createdExp = -int(len(strCreatedSecFraction))
184                createdSecFraction = int(strCreatedSecFraction) *10**createdExp
185               
186            except ValueError, e:
187                raise ValueError("Parsing timestamp Created element: %s" % e)
188           
189   
190            dtCreated = _strptime(createdDateTime, '%Y-%m-%dT%H:%M:%S')
191            dtCreated += timedelta(seconds=createdSecFraction)
192            if dtCreated >= dtNow:
193                raise TimestampError("Timestamp created time %s is equal to "
194                                     "or after the current time %s" %
195                                     (dtCreated, dtNow))
196       
197        expiresElem = timestampElem.find("wsu:Expires",
198                                         namespaces=self._processorNSs)
199        if expiresElem is None:
200            msg = "No <wsu:Expires/> element found in Timestamp"
201            if expiresElemMustBeSet:
202                raise TimestampError(msg)
203            else:
204                log.warning(msg)
205        else:
206            try:
207                expiryDateTime, strExpirySecFraction = \
208                                                    expiresElem.text.split('.')
209                strExpirySecFraction = strExpirySecFraction.split('Z')[0]
210                expiryExp = -int(len(strExpirySecFraction))
211                expirySecFraction = int(strExpirySecFraction) * 10 ** expiryExp
212               
213            except ValueError, e:
214                raise ValueError("Parsing timestamp Expires element: %s" % e)
215           
216            dtExpiry = _strptime(expiryDateTime, '%Y-%m-%dT%H:%M:%S')
217            dtExpiry += timedelta(seconds=expirySecFraction)
218            if dtExpiry < dtNow:
219                raise MessageExpired("Message has expired: timestamp expiry "
220                                     "time %s is before the current time %s" % 
221                                     (dtCreated, dtNow))
222           
223        log.debug("Completed timestamp verification")
224           
225           
226    def sign(self, soapWriter):
227        '''Sign the message body and binary security token of a SOAP message
228       
229        @type soapWriter: ZSI.writer.SoapWriter
230        @param soapWriter: ZSI object to write SOAP message
231        '''
232       
233        # Check for expected soapWriter DOM class
234        if not isinstance(soapWriter.dom, ElementTreeProxy):
235            raise SignatureError("Expecting ElementTreeProxy type for "
236                                 "ZSI.writer.SoapWriter.dom instance")
237           
238        # Add X.509 cert as binary security token
239        # TODO: sub encodestring with b64encode?
240        if self.reqBinSecTokValType==self.binSecTokValType['X509PKIPathv1']:
241            binSecTokVal = base64.encodestring(self.signingCertChain.asDER())
242        else:
243            # Assume X.509 / X.509 vers 3
244            binSecTokVal = base64.encodestring(self.signingCert.asDER())
245
246        self._soapEnvElem = soapWriter.dom._elem
247        soapHdrElem = soapWriter._header._elem
248        soapBodyElem = soapWriter.body._elem
249       
250        self._soapEnvElem.set("xmlns:%s" % 'ds', DSIG.BASE)
251        #self._soapEnvElem.set("xmlns:%s" % 'wsse', OASIS.WSSE)
252       
253        soapHdrElem.set("xmlns:%s" % 'wsse', OASIS.WSSE)
254        soapHdrElem.set("xmlns:%s" % 'wsse11', OASIS.WSSE11)
255#        soapHdrElem.set("xmlns:%s" % 'wsu', _WSU.UTILITY)
256        self._soapEnvElem.set("xmlns:%s" % 'wsu', _WSU.UTILITY)
257        soapHdrElem.set("xmlns:%s" % 'ds', DSIG.BASE)
258       
259        refC14nPfxSet = False
260        if isinstance(self.refC14nKw.get('inclusive_namespaces'), list) and \
261            len(self.refC14nKw['inclusive_namespaces']) > 0:
262            refC14nPfxSet = True 
263
264        signedInfoC14nPfxSet = False
265        if isinstance(self.signedInfoC14nKw.get('inclusive_namespaces'), list) and \
266            len(self.signedInfoC14nKw['inclusive_namespaces']) > 0:
267            signedInfoC14nPfxSet = True
268               
269        if refC14nPfxSet or signedInfoC14nPfxSet:
270            soapHdrElem.set("xmlns:%s" % 'ec', DSIG.C14N_EXCL)
271           
272           
273        # Check <wsse:security> isn't already present in header
274        wsseElems = self._soapEnvElem.findall('.//wsse:security',
275                                              namespaces=self._processorNSs)
276        if len(wsseElems) > 1:
277            raise SignatureError('wsse:Security element is already present')
278       
279        # Add WSSE element
280        wsseElem = ElementTree.Element("{%s}%s" % (OASIS.WSSE, 'Security'))
281        soapHdrElem.append(wsseElem)
282       
283        wsseElem.set("xmlns:%s" % 'wsse', OASIS.WSSE)
284       
285        # Recipient MUST parse and check this signature
286        wsseElem.set('SOAP-ENV:mustUnderstand', "1")
287       
288        # Binary Security Token element will contain the X.509 cert
289        # corresponding to the private key used to sing the message
290        binSecTokElem = ElementTree.Element("{%s}%s" % (OASIS.WSSE, 
291                                                        'BinarySecurityToken'))
292        wsseElem.append(binSecTokElem)
293       
294        # Value type can be any be any one of those supported via
295        # binSecTokValType
296        binSecTokElem.set('ValueType', self.reqBinSecTokValType)
297        binSecTokElem.set('EncodingType', self._binSecTokEncType)
298       
299        # Add ID so that the binary token can be included in the signature
300        binSecTokElem.set('{%s}Id' % _WSU.UTILITY, "binaryToken")
301       
302        binSecTokElem.text = binSecTokVal
303
304
305        # Timestamp
306        if self.addTimestamp:
307            self._addTimeStamp(wsseElem)
308           
309        # Signature Confirmation
310        if self.applySignatureConfirmation: 
311            self._applySignatureConfirmation(wsseElem)
312       
313        # Signature
314        signatureElem = ElementTree.Element("{%s}%s" % (DSIG.BASE,'Signature'))
315        wsseElem.append(signatureElem)
316       
317        # Signature - Signed Info
318        signedInfoElem = ElementTree.Element("{%s}%s"%(DSIG.BASE,'SignedInfo'))
319        signatureElem.append(signedInfoElem)
320       
321        # Signed Info - Canonicalization method
322        c14nMethodElem = ElementTree.Element("{%s}%s" % (DSIG.BASE,
323                                                    'CanonicalizationMethod'))
324        signedInfoElem.append(c14nMethodElem)
325       
326        # Set based on 'signedInfoIsExcl' property
327        c14nAlgOpt = (DSIG.C14N, DSIG.C14N_EXCL)
328        signedInfoC14nAlg = c14nAlgOpt[int(self.signedInfoC14nIsExcl)]
329       
330        c14nMethodElem.set('Algorithm', signedInfoC14nAlg)
331       
332        if signedInfoC14nPfxSet:
333            c14nInclNamespacesElem = ElementTree.Element("{%s}%s" % \
334                                                    (signedInfoC14nAlg,
335                                                    'InclusiveNamespaces'))
336            c14nMethodElem.append(c14nInclNamespacesElem)
337           
338            pfxList = ' '.join(self.signedInfoC14nKw['inclusive_namespaces'])
339            c14nInclNamespacesElem.set('PrefixList', pfxList)
340
341       
342        # Signed Info - Signature method
343        sigMethodElem = ElementTree.Element("{%s}%s" % \
344                                            (DSIG.BASE, 'SignatureMethod'))
345        signedInfoElem.append(sigMethodElem)
346        sigMethodElem.set('Algorithm', DSIG.SIG_RSA_SHA1)
347       
348        # Signature - Signature value
349        signatureValueElem = ElementTree.Element("{%s}%s" % (DSIG.BASE, 
350                                                             'SignatureValue'))
351        signatureElem.append(signatureValueElem)
352       
353        # Key Info
354        KeyInfoElem = ElementTree.Element("{%s}%s" % (DSIG.BASE, 'KeyInfo'))
355        signatureElem.append(KeyInfoElem)
356       
357        secTokRefElem = ElementTree.Element("{%s}%s" % (OASIS.WSSE, 
358                                                    'SecurityTokenReference'))
359        KeyInfoElem.append(secTokRefElem)
360       
361        # Reference back to the binary token included earlier
362        wsseRefElem = ElementTree.Element("{%s}%s" % (OASIS.WSSE, 'Reference'))
363        secTokRefElem.append(wsseRefElem)
364       
365        wsseRefElem.set('URI', "#binaryToken")
366       
367        # Add Reference to body so that it can be included in the signature
368        #soapBodyElem.set('xmlns:wsu', _WSU.UTILITY) - results in duplicate xmlns declarations
369        soapBodyElem.set('{%s}Id' % _WSU.UTILITY, 'body')
370
371
372        # Set Reference Canonicalization algorithm based on 'refC14nIsExcl'
373        # property
374        refC14nAlg = c14nAlgOpt[self.refC14nIsExcl]
375       
376        # Pick up all the wsu:Id tagged elements set in the above
377        refElems = self._soapEnvElem.findall('.//*[@wsu:Id]',
378                                             namespaces=self._processorNSs)
379
380        # 1) Reference Generation
381        #
382        # Find references
383        for refElem in refElems:
384           
385            # Set URI attribute to point to reference to be signed
386            uri = '#' + refElem.get('{%s}%s' % (_WSU.UTILITY, 'Id'))
387           
388            # Canonicalize reference
389            refC14n = soapWriter.dom.canonicalize(subset=refElem,
390                                                  exclusive=self.refC14nIsExcl,
391                                                  **self.refC14nKw)
392            log.debug('Canonicalisation for URI "%s": %s', uri, refC14n)
393           
394            # Calculate digest for reference and base 64 encode
395            #
396            # Nb. encodestring adds a trailing newline char
397            # Use b64encode instead - encodestring puts in newline chars at
398            # 76 char intervals
399            #digestValue = base64.encodestring(sha(refC14n).digest()).strip()
400            digestValue = base64.b64encode(sha(refC14n).digest())
401           
402            # Add a new reference element to SignedInfo
403            signedInfoRefElem = ElementTree.Element("{%s}Reference"%DSIG.BASE)
404            signedInfoElem.append(signedInfoRefElem)
405            signedInfoRefElem.set('URI', uri)
406           
407            # Use ds:Transforms or wsse:TransformationParameters?
408            transformsElem = ElementTree.Element("{%s}Transforms" % DSIG.BASE)
409            signedInfoRefElem.append(transformsElem)
410           
411            transformElem = ElementTree.Element("{%s}Transform" % DSIG.BASE)
412            transformsElem.append(transformElem)
413
414            # Set Canonicalization algorithm type
415            transformElem.set('Algorithm', refC14nAlg)
416            if refC14nPfxSet:
417                # Exclusive C14N requires inclusive namespace elements
418                inclNamespacesElem = transformElem.createAppendElement(\
419                                                       refC14nAlg,
420                                                       'InclusiveNamespaces')
421                refPfxList = ' '.join(self.refC14nKw['inclusive_namespaces'])
422                inclNamespacesElem.set('PrefixList', refPfxList)
423           
424            # Digest Method
425            digestMethodElem = ElementTree.Element("{%s}%s" % (DSIG.BASE, 
426                                                               'DigestMethod'))
427            signedInfoRefElem.append(digestMethodElem)
428           
429            digestMethodElem.set('Algorithm', DSIG.DIGEST_SHA1)
430           
431            # Digest Value
432            digestValueElem = ElementTree.Element("{%s}%s" % (DSIG.BASE, 
433                                                              'DigestValue'))
434            signedInfoRefElem.append(digestValueElem)
435            digestValueElem.text = digestValue
436   
437        # 2) Signature Generation
438        #       
439        # Canonicalize the signedInfo node
440        c14nSignedInfo = soapWriter.dom.canonicalize(subset=signedInfoElem,
441                                           exclusive=self.signedInfoC14nIsExcl,
442                                           **self.signedInfoC14nKw)
443        log.debug('Canonicalisation for <ds:signedInfo>: %s', c14nSignedInfo)
444       
445        # Calculate digest of SignedInfo
446        signedInfoDigestValue = sha(c14nSignedInfo).digest()
447       
448        # Sign using the private key and base 64 encode the result
449        signatureValue = self.signingPriKey.sign(signedInfoDigestValue)
450       
451        # encodestring puts newline markers at 76 char intervals otherwise no
452        # difference
453        # b64EncSignatureValue = base64.encodestring(signatureValue).strip()
454        b64EncSignatureValue = base64.b64encode(signatureValue)
455
456        # Add to <SignatureValue>
457        signatureValueElem.text = b64EncSignatureValue
458        log.debug("Signature generation complete")
459
460
461    def verify(self, parsedSOAP):
462        """Verify signature
463       
464        @type parsedSOAP: ZSI.parse.ParsedSoap
465        @param parsedSOAP: object contain parsed SOAP message received from
466        sender"""
467
468        if not isinstance(parsedSOAP.dom, ElementTreeProxy):
469            raise VerifyError("Expecting ElementTreeProxy type for "
470                              "ZSI.parse.ParsedSoap.dom")
471       
472        self._soapEnvElem = parsedSOAP.dom._elem
473
474        signatureElems = self._soapEnvElem.findall('.//ds:Signature', 
475                                                namespaces=self._processorNSs)       
476        if len(signatureElems) > 1:
477            raise VerifyError('Multiple <ds:Signature/> elements found')
478       
479        try:
480            signatureElems = signatureElems[0]
481        except:
482            # Message wasn't signed - may be possible if peer raised a SOAP
483            # fault
484            raise NoSignatureFound("Input message wasn't signed!")
485
486       
487        # Two stage process: reference validation followed by signature
488        # validation
489       
490        # 1) Reference Validation
491       
492        # Check for canonicalization set via ds:CanonicalizationMethod -
493        # Use this later as a back up in case no Canonicalization was set in
494        # the transforms elements
495        c14nMethodElem = self._soapEnvElem.find('.//ds:CanonicalizationMethod', 
496                                                namespaces=self._processorNSs)
497        if c14nMethodElem is None:
498            raise VerifyError("No <ds:Canonicalization/> element found") 
499             
500        refElems = self._soapEnvElem.findall('.//ds:Reference', 
501                                             namespaces=self._processorNSs)
502        if len(refElems) == 0:
503            raise VerifyError("No <ds:Reference/> elements found") 
504       
505        for refElem in refElems:
506            # Get the URI for the reference
507            refURI = refElem.get('URI')
508                         
509            transformElem = refElem.find('ds:Transforms/ds:Transform',
510                                         namespaces=self._processorNSs)
511            if transformElem is None:
512                raise VerifyError('Failed to get transform algorithm for '
513                                  '<ds:Reference URI="%s">' % refURI)
514               
515            refAlgorithm = transformElem.get("Algorithm")
516               
517            # Add extra keyword for Exclusive canonicalization method
518            refC14nKw = dict(exclusive=refAlgorithm == DSIG.C14N_EXCL)
519            if refC14nKw['exclusive']:
520                # Check for no inclusive namespaces set
521                inclusiveNSElem = transformElem.find("InclusiveNamespaces",
522                                                namespaces=self._processorNSs)                   
523                if inclusiveNSElem is not None:
524                    pfxListTxt = inclusiveNSElem.get('PrefixList')
525                    if pfxListTxt is None:
526                        raise VerifyError('Empty InclusiveNamespaces list for'\
527                                          ' <ds:Reference URI="%s">' % refURI)
528                                                 
529                    refC14nKw['inclusive_namespaces'] = pfxListTxt.split()
530                else:
531                    # Set to empty list to ensure Exclusive C14N is set for
532                    # Canonicalize call
533                    refC14nKw['inclusive_namespaces'] = []
534       
535            # Canonicalize the reference data and calculate the digest
536            if refURI[0] != "#":
537                raise VerifyError('Expecting # identifier for Reference URI' \
538                                  ' "%s"' % refURI)
539                   
540            # XPath reference
541            uriXPath = './/*[@wsu:Id="%s"]' % refURI[1:]
542            uriElem=self._soapEnvElem.findall(uriXPath,
543                                              namespaces=self._processorNSs)
544            if len(uriElem) > 1:
545                raise VerifyError("Multiple elements matching '%s' search" 
546                                  " path: %s" % (uriXPath, uriElem))
547
548            refC14n=parsedSOAP.dom.canonicalize(subset=uriElem[0], **refC14nKw)
549           
550            # encodestring adds line delimiters at 76 char intervals - avoid
551            # and use b64encode instead           
552            calculatedDigestValue = base64.b64encode(sha(refC14n).digest())
553           
554            # Extract the digest value that was stored in the SOAP request         
555            digestElem = refElem.find('ds:DigestValue',
556                                      namespaces=self._processorNSs)
557            if digestElem is None:
558                raise VerifyError('Failed to get digestValue for ' \
559                                  '<ds:Reference URI="%s">' % refURI)
560               
561            # Need to check here for value split into separate lines?
562            retrievedDigestValue = str(digestElem.text).strip()   
563           
564            # Reference validates if the two digest values are the same
565            if retrievedDigestValue != calculatedDigestValue:
566                log.error("Digest values don't match")
567                log.error('Canonicalisation for URI: "%s": %s' % \
568                          (refURI, refC14n))
569                raise InvalidSignature('Digest Values do not match for URI:' \
570                                       ' "%s"' % refURI)
571           
572            log.debug("Verified canonicalization for element '%s'"%refURI[1:])
573           
574
575        # 2) Signature Validation
576        signedInfoElem = self._soapEnvElem.find('.//ds:SignedInfo',
577                                                namespaces=self._processorNSs)
578        if signedInfoElem is None:
579            raise VerifyError("No <ds:signedInfo/> section found")
580       
581        # Get algorithm used for canonicalization of the SignedInfo
582        # element.  Nb. This is NOT necessarily the same as that used to
583        # canonicalize the reference elements checked above!
584        signedInfoC14nAlg = c14nMethodElem.get("Algorithm")
585        if signedInfoC14nAlg is None:
586            raise VerifyError('No Algorithm attribute set for <signedInfo/>' \
587                              ' section')
588           
589        signedInfoC14nKw = dict(exclusive=signedInfoC14nAlg == DSIG.C14N_EXCL)
590        if signedInfoC14nKw['exclusive']:
591
592            # Check for no inclusive namespaces set
593            inclusiveNSElem = c14nMethodElem.find("InclusiveNamespaces",
594                                                namespaces=self._processorNSs)                   
595            if inclusiveNSElem is not None:
596                pfxListTxt = inclusiveNSElem.get('PrefixList')
597                if pfxListTxt is None:
598                    raise VerifyError('Empty InclusiveNamespaces list for'\
599                                      ' <ds:Reference URI="%s">' % refURI)
600                                             
601                signedInfoC14nKw['inclusive_namespaces'] = pfxListTxt.split()
602            else:
603                # Set to empty list to ensure Exclusive C14N is set for
604                # Canonicalize call
605                signedInfoC14nKw['inclusive_namespaces'] = []
606
607        # Canonicalize the SignedInfo node and take digest
608        c14nSignedInfo = parsedSOAP.dom.canonicalize(subset=signedInfoElem, 
609                                                     **signedInfoC14nKw)                     
610        signedInfoDigestValue = sha(c14nSignedInfo).digest()
611       
612        # Get the signature value in order to check against the digest just
613        # calculated
614        signatureValueElem = self._soapEnvElem.find('.//ds:SignatureValue',
615                                                namespaces=self._processorNSs)
616
617        # Remove base 64 encoding
618        b64EncSignatureValue = signatureValueElem.text
619        signatureValue = base64.decodestring(b64EncSignatureValue)
620
621        # Cache Signature Value here so that a response can include it
622        if self.applySignatureConfirmation:
623            # re-encode string to avoid possible problems with interpretation
624            # of line breaks
625            self.b64EncSignatureValue = b64EncSignatureValue
626        else:
627            self.b64EncSignatureValue = None
628         
629        # Look for X.509 Cert in wsse:BinarySecurityToken element -
630        # Signature may not have included the Binary Security Token in
631        # which case the verifying cert will need to have been set
632        # elsewhere
633        binSecTokElem = self._soapEnvElem.find('.//wsse:BinarySecurityToken',
634                                               namespaces=self._processorNSs)       
635        if binSecTokElem is not None:
636            x509CertTxt = str(binSecTokElem.text)
637           
638            valueType = binSecTokElem.get("ValueType")
639            if valueType in (self.binSecTokValType['X509v3'],
640                             self.binSecTokValType['X509']):
641                # Remove base 64 encoding
642                derString = base64.decodestring(x509CertTxt)
643                self.verifyingCert = X509Cert.Parse(derString, 
644                                                    format=X509Cert.formatDER)
645               
646                x509Stack = X509Stack()
647
648            elif valueType == self.binSecTokValType['X509PKIPathv1']:
649               
650                derString = base64.decodestring(x509CertTxt)
651                x509Stack = X509StackParseFromDER(derString)
652               
653                # TODO: Check ordering - is the last off the stack the
654                # one to use to verify the message?
655                self.verifyingCert = x509Stack[-1]
656            else:
657                raise WSSecurityError('BinarySecurityToken ValueType '
658                                      'attribute is not recognised: "%s"' % 
659                                      valueType)
660
661        if self.verifyingCert is None:
662            raise VerifyError("No certificate set for verification of the "
663                              "signature")
664       
665        # Extract RSA public key from the cert
666        rsaPubKey = self.verifyingCert.pubKey.get_rsa()
667
668        # Apply the signature verification
669        try:
670            verify = rsaPubKey.verify(signedInfoDigestValue, signatureValue)
671        except RSA.RSAError, e:
672            raise VerifyError("Error in Signature: " + str(e))
673       
674        if not verify:
675            raise InvalidSignature("Invalid signature")
676       
677        log.debug("Verified signature")
678       
679        # Verify chain of trust
680        x509Stack.verifyCertChain(x509Cert2Verify=self.verifyingCert,
681                                  caX509Stack=self.__caX509Stack)
682
683        log.debug("Verified certificate chain of trust")
684       
685        self._verifyTimeStamp(parsedSOAP,
686                              timestampMustBeSet=self.timestampMustBeSet,
687                              createdElemMustBeSet=self.createdElemMustBeSet,
688                              expiresElemMustBeSet=self.expiresElemMustBeSet) 
689
690        log.info("Signature OK")       
691
692
693    def canonicalize(self, **kw):
694        '''ElementTree based Canonicalization - See ElementC14N for keyword
695        info'''
696        f = StringIO()
697        ElementC14N.write(ElementC14N.build_scoped_tree(self._soapEnvElem), 
698                          f, 
699                          **kw)
700        return f.getvalue()
Note: See TracBrowser for help on using the repository browser.