source: TI12-security/trunk/python/ndg.security.common/ndg/security/common/wssecurity/dom.py @ 4672

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg/TI12-security/trunk/python/ndg.security.common/ndg/security/common/wssecurity/dom.py@4672
Revision 4672, 46.8 KB checked in by pjkersha, 13 years ago (diff)

Re-run unit tests following fixes:

  • BaseSignatureHandler?.SignatureHandler? - important fix to setCert method
  • ndg.security.server.wsgi.openid.provider.OpenIDProviderMiddleware fix to _filterOpts
  • update to remaining certs due to expire in the unit tests dirs.
  • Property svn:executable set to *
Line 
1""" DOM based WS-Security digital signature handler
2
3NERC Data Grid Project
4"""
5__author__ = "C Byrom"
6__date__ = "18/08/08"
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__ = "Philip.Kershaw@stfc.ac.uk"
12__revision__ = '$Id: $'
13
14
15import re
16
17# Digest and signature/verify
18from sha import sha
19from M2Crypto import X509, BIO, RSA
20import base64
21
22# Conditional import as this is required for the encryption
23# handler
24try:
25    # For shared key encryption
26    from Crypto.Cipher import AES, DES3
27except:
28    from warnings import warn
29    warn('Crypto.Cipher not available: EncryptionHandler disabled!',
30         RuntimeWarning)
31    class AES:
32        MODE_ECB = None
33        MODE_CBC = None
34       
35    class DES3: 
36        MODE_CBC = None
37
38import os
39
40import ZSI
41from ZSI.wstools.Namespaces import DSIG, WSA200403, \
42                                   SOAP, SCHEMA # last included for xsi
43
44from ZSI.TC import ElementDeclaration,TypeDefinition
45from ZSI.generate.pyclass import pyclass_type
46
47from ZSI.wstools.Utility import DOMException
48from ZSI.wstools.Utility import NamespaceError, MessageInterface, ElementProxy
49
50# Canonicalization
51from ZSI.wstools.c14n import Canonicalize
52
53from xml.dom import Node
54from xml.xpath.Context import Context
55from xml import xpath
56
57# Include for re-parsing doc ready for canonicalization in sign method - see
58# associated note
59from xml.dom.ext.reader.PyExpat import Reader
60
61from datetime import datetime, timedelta
62import logging
63log = logging.getLogger(__name__)
64
65from BaseSignatureHandler import _ENCRYPTION, _WSU, OASIS, \
66    BaseSignatureHandler, WSSecurityError, NoSignatureFound, InvalidSignature,\
67    TimestampError, VerifyError, SignatureError
68
69# Enable settings from a config file
70from ndg.security.common.wssecurity import WSSecurityConfig
71
72from ndg.security.common.X509 import X509Cert, X509CertParse, X509CertRead, \
73X509Stack, X509StackParseFromDER
74
75class SignatureHandler(BaseSignatureHandler):
76    """Class to handle signature and verification of signature with
77    WS-Security
78    """
79
80    def _applySignatureConfirmation(self, wsseElem):
81        '''Add SignatureConfirmation element - as specified in WS-Security 1.1
82        - to outbound message on receipt of a signed message from a client
83       
84        This has been added in through tests vs. Apache Axis Rampart client
85       
86        @type wsseElem:
87        @param wsseElem: wsse:Security element'''
88        if self.b64EncSignatureValue is None:
89            log.info("SignatureConfirmation element requested but no request "
90                     "signature was cached")
91            return
92       
93        sigConfirmElem = wsseElem.createAppendElement(OASIS.WSSE11, 
94                                                      'SignatureConfirmation')
95       
96        # Add ID so that the element can be included in the signature
97        sigConfirmElem.node.setAttribute('wsu:Id', "signatureConfirmation")
98
99        # Add ID so that the element can be included in the signature
100        # Following line is a hck to avoid appearance of #x when serialising \n
101        # chars TODO: why is this happening??
102        b64EncSignatureValue = ''.join(self.b64EncSignatureValue.split('\n'))
103        sigConfirmElem.node.setAttribute('Value', b64EncSignatureValue)
104       
105       
106    def _addTimeStamp(self, wsseElem, elapsedSec=60*5):
107        '''Add a timestamp to wsse:Security section of message to be signed
108        e.g.
109            <wsu:Timestamp wsu:Id="timestamp">
110               <wsu:Created>2008-03-25T14:40:37.319Z</wsu:Created>
111               <wsu:Expires>2008-03-25T14:45:37.319Z</wsu:Expires>
112            </wsu:Timestamp>
113       
114        @type wsseElem:
115        @param wsseElem: wsse:Security element
116        @type elapsedSec: int   
117        @param elapsedSec: time interval in seconds between Created and Expires
118        time stamp values
119        '''
120        # Nb. wsu ns declaration is in the SOAP header elem
121        timestampElem = wsseElem.createAppendElement(_WSU.UTILITY, 'Timestamp')
122
123        # Add ID so that the timestamp element can be included in the signature
124        timestampElem.node.setAttribute('wsu:Id', "timestamp")
125       
126        # Value type can be any be any one of those supported via
127        # binSecTokValType
128        createdElem = timestampElem.createAppendElement(_WSU.UTILITY,'Created')
129        dtCreatedTime = datetime.utcnow()
130        createdElem.createAppendTextNode(dtCreatedTime.isoformat('T')+'Z')
131       
132        dtExpiryTime = dtCreatedTime + timedelta(seconds=elapsedSec)
133        expiresElem = timestampElem.createAppendElement(_WSU.UTILITY,'Expires')
134        expiresElem.createAppendTextNode(dtExpiryTime.isoformat('T')+'Z')
135       
136
137    def _verifyTimeStamp(self, parsedSOAP, ctxt, timestampMustBeSet=False):
138        """Call from verify to check timestamp if found. 
139       
140        TODO: refactor input args - maybe these should by object attributes
141       
142        @type parsedSOAP: ZSI.parse.ParsedSoap
143        @param parsedSOAP: object contain parsed SOAP message received from
144        sender
145        @type ctxt:
146        @param ctxt: XPath context object"""
147
148        # TODO: do we need to be more rigorous in terms of handling the
149        # situation where no timestamp is found?
150       
151        try:
152            timestampNode = xpath.Evaluate('//wsu:Timestamp',
153                                           contextNode=parsedSOAP.dom,
154                                           context=ctxt)[0]
155        except:
156            msg = "Verifying message - No timestamp element found"
157            if timestampMustBeSet:
158                raise TimestampError(msg)
159            else:
160                log.warning(msg)
161                return
162       
163        # Time now
164        dtNow = datetime.utcnow()
165
166        createdNode = timestampNode.getElementsByTagName("wsu:Created")
167        if createdNode is None:
168            raise TimestampError("Verifying message - No Created timestamp "
169                                 "sub-element found")
170           
171        # Workaround for fractions of second
172        try:
173            createdDateTime, createdSecFraction = \
174                            createdNode[0].childNodes[0].nodeValue.split('.')
175            dtCreated = datetime.strptime(createdDateTime, '%Y-%m-%dT%H:%M:%S')
176            createdSeconds = float("0." + createdSecFraction.replace('Z', ''))
177            dtCreated += timedelta(seconds=createdSeconds)
178                                           
179        except ValueError, e:
180            raise TimestampError("Failed to parse timestamp Created element: "
181                                 "%s" % e)
182       
183        if dtCreated >= dtNow:
184            raise TimestampError("Timestamp created time %s is equal to or "
185                                 "after the current time %s" % \
186                                 (dtCreated, dtNow))
187       
188        expiresNode = timestampNode.getElementsByTagName("wsu:Expires")
189        if expiresNode is None:
190            log.warning("Verifying message - No Expires element found in "
191                        "Timestamp")
192            return
193
194        try:
195            expiresDateTime, expiresSecFraction = \
196                            expiresNode[0].childNodes[0].nodeValue.split('.')
197            dtExpiry = datetime.strptime(expiresDateTime, '%Y-%m-%dT%H:%M:%S')
198            expirySeconds = float("0." + expiresSecFraction.replace('Z', ''))
199            dtExpiry += timedelta(seconds=expirySeconds)
200
201        except ValueError, e:
202            raise TimestampError("Failed to parse timestamp Expires element: "
203                                 "%s" % e)
204
205        if dtExpiry < dtNow:
206            raise TimestampError("Timestamp expiry time %s is before the "
207                                 "current time %s - i.e. the message has "
208                                 "expired." % (dtExpiry, dtNow))
209           
210                   
211    def sign(self, soapWriter):
212        '''Sign the message body and binary security token of a SOAP message
213       
214        @type soapWriter: ZSI.writer.SoapWriter
215        @param soapWriter: ZSI object to write SOAP message
216        '''
217       
218        # Namespaces for XPath searches
219        processorNss = \
220        {
221            'ds':     DSIG.BASE, 
222            'wsu':    _WSU.UTILITY, 
223            'wsse':   OASIS.WSSE, 
224            'soapenv':"http://schemas.xmlsoap.org/soap/envelope/" 
225        }
226
227        # Add X.509 cert as binary security token
228        if self.reqBinSecTokValType==self.binSecTokValType['X509PKIPathv1']:
229            if self.signingCertChain is None:
230                msg = 'SignatureHandler signingCertChain attribute is not set'
231                log.error(msg)
232                raise AttributeError(msg)
233           
234            binSecTokVal = base64.encodestring(self.signingCertChain.asDER())
235        else:
236            # Assume X.509 / X.509 vers 3
237            if self.signingCert is None:
238                msg = 'SignatureHandler signingCert attribute is not set'
239                log.error(msg)
240                raise AttributeError(msg)
241           
242            binSecTokVal = base64.encodestring(self.signingCert.asDER())
243
244        soapWriter._header.setNamespaceAttribute('wsse', OASIS.WSSE)
245        soapWriter._header.setNamespaceAttribute('wsse11', OASIS.WSSE11)
246        soapWriter._header.setNamespaceAttribute('wsu', _WSU.UTILITY)
247        soapWriter._header.setNamespaceAttribute('ds', DSIG.BASE)
248       
249        refC14nPfxSet = False
250        if self.refC14nIsExcl:
251            refC14nPfxSet = True 
252
253        signedInfoC14nPfxSet = False
254        if self.signedInfoC14nIsExcl:
255            signedInfoC14nPfxSet = True
256               
257        if refC14nPfxSet or signedInfoC14nPfxSet:
258           soapWriter._header.setNamespaceAttribute('ec', DSIG.C14N_EXCL)
259       
260        # Check <wsse:security> isn't already present in header
261        ctxt = Context(soapWriter.dom.node, processorNss=processorNss)
262        wsseNodes = xpath.Evaluate('//wsse:security', 
263                                   contextNode=soapWriter.dom.node, 
264                                   context=ctxt)
265        if len(wsseNodes) > 1:
266            raise SignatureError('wsse:Security element is already present')
267
268        # Add WSSE element
269        wsseElem = soapWriter._header.createAppendElement(OASIS.WSSE, 
270                                                          'Security')
271        wsseElem.setNamespaceAttribute('wsse', OASIS.WSSE)
272       
273        # Recipient MUST parse and check this signature
274        wsseElem.node.setAttribute('SOAP-ENV:mustUnderstand', "1")
275       
276        # Binary Security Token element will contain the X.509 cert
277        # corresponding to the private key used to sing the message
278        binSecTokElem = wsseElem.createAppendElement(OASIS.WSSE, 
279                                                     'BinarySecurityToken')
280       
281        # Value type can be any be any one of those supported via
282        # binSecTokValType
283        binSecTokElem.node.setAttribute('ValueType', 
284                                        self.reqBinSecTokValType)
285
286        binSecTokElem.node.setAttribute('EncodingType', self._binSecTokEncType)
287       
288        # Add ID so that the binary token can be included in the signature
289        binSecTokElem.node.setAttribute('wsu:Id', "binaryToken")
290
291        binSecTokElem.createAppendTextNode(binSecTokVal)
292
293
294        # Timestamp
295        if self.addTimestamp:
296            self._addTimeStamp(wsseElem)
297           
298        # Signature Confirmation
299        if self.applySignatureConfirmation: 
300            self._applySignatureConfirmation(wsseElem)
301       
302        # Signature
303        signatureElem = wsseElem.createAppendElement(DSIG.BASE, 'Signature')
304        signatureElem.setNamespaceAttribute('ds', DSIG.BASE)
305       
306        # Signature - Signed Info
307        signedInfoElem = signatureElem.createAppendElement(DSIG.BASE, 
308                                                           'SignedInfo')
309       
310        # Signed Info - Canonicalization method
311        c14nMethodElem = signedInfoElem.createAppendElement(DSIG.BASE,
312                                                    'CanonicalizationMethod')
313       
314        # Set based on 'signedInfoIsExcl' property
315        c14nAlgOpt = (DSIG.C14N, DSIG.C14N_EXCL)
316        signedInfoC14nAlg = c14nAlgOpt[int(self.signedInfoC14nIsExcl)]
317
318        log.warning("Forcing use of Exclusive C14N for SignedInfo section - "
319                    "Inclusive C14N not working")
320       
321        # TODO: remove this line if ZSI.Canonicalize ever starts working with
322        # inclusive NS
323        signedInfoC14nAlg = c14nAlgOpt[1]
324       
325        c14nMethodElem.node.setAttribute('Algorithm', signedInfoC14nAlg)
326       
327        if signedInfoC14nPfxSet:
328            c14nInclNamespacesElem = c14nMethodElem.createAppendElement(
329                                                    signedInfoC14nAlg,
330                                                    'InclusiveNamespaces')
331            c14nInclNamespacesElem.node.setAttribute('PrefixList', 
332                            ' '.join(self.signedInfoC14nKw['inclusive_namespaces']))
333       
334        # Signed Info - Signature method
335        sigMethodElem = signedInfoElem.createAppendElement(DSIG.BASE,
336                                                           'SignatureMethod')
337        sigMethodElem.node.setAttribute('Algorithm', DSIG.SIG_RSA_SHA1)
338       
339        # Signature - Signature value
340        signatureValueElem = signatureElem.createAppendElement(DSIG.BASE, 
341                                                             'SignatureValue')
342       
343        # Key Info
344        KeyInfoElem = signatureElem.createAppendElement(DSIG.BASE, 'KeyInfo')
345        secTokRefElem = KeyInfoElem.createAppendElement(OASIS.WSSE, 
346                                                  'SecurityTokenReference')
347       
348        # Reference back to the binary token included earlier
349        wsseRefElem = secTokRefElem.createAppendElement(OASIS.WSSE, 
350                                                        'Reference')
351        wsseRefElem.node.setAttribute('URI', "#binaryToken")
352       
353        # Add Reference to body so that it can be included in the signature
354        soapWriter.body.node.setAttribute('wsu:Id', "body")
355        soapWriter.body.node.setAttribute('xmlns:wsu', _WSU.UTILITY)
356
357        # Serialize and re-parse prior to reference generation - calculating
358        # canonicalization based on soapWriter.dom.node seems to give an
359        # error: the order of wsu:Id attribute is not correct
360        try:
361            docNode = Reader().fromString(str(soapWriter))
362        except Exception, e:
363            raise SignatureError("Error parsing SOAP message for signing: %s"%\
364                                 e)
365
366        ctxt = Context(docNode, processorNss=processorNss)
367        refNodes = xpath.Evaluate('//*[@wsu:Id]', 
368                                  contextNode=docNode, 
369                                  context=ctxt)
370
371        # Set based on 'signedInfoIsExcl' property
372        refC14nAlg = c14nAlgOpt[self.refC14nIsExcl]
373
374        log.warning("Forcing use of Exclusive C14N for references - Inclusive "
375                    "C14N not working")
376       
377        # TODO: remove this line if ZSI.Canonicalize ever starts working with
378        # inclusive NS
379        refC14nAlg = c14nAlgOpt[1]
380       
381        # 1) Reference Generation
382        #
383        # Find references
384        for refNode in refNodes:
385           
386            refID = refNode.attributes[(_WSU.UTILITY, 'Id')].value
387           
388            # Set URI attribute to point to reference to be signed
389            uri = u"#" + refID
390           
391            # Canonicalize reference
392            inclusiveNSKWs = self.createUnsupressedPrefixKW(self.refC14nKw)
393            refSubsetList = getChildNodes(refNode)
394            refC14n = Canonicalize(docNode, 
395                                   None, 
396                                   subset=refSubsetList,
397                                   **inclusiveNSKWs)
398           
399            # Calculate digest for reference and base 64 encode
400            #
401            # Nb. encodestring adds a trailing newline char
402            digestValue = base64.encodestring(sha(refC14n).digest()).strip()
403
404
405            # Add a new reference element to SignedInfo
406            refElem = signedInfoElem.createAppendElement(DSIG.BASE, 
407                                                         'Reference')
408            refElem.node.setAttribute('URI', uri)
409           
410            # Use ds:Transforms or wsse:TransformationParameters?
411            transformsElem = refElem.createAppendElement(DSIG.BASE, 
412                                                         'Transforms')
413            transformElem = transformsElem.createAppendElement(DSIG.BASE, 
414                                                               'Transform')
415
416            # Set Canonicalization algorithm type
417            transformElem.node.setAttribute('Algorithm', refC14nAlg)
418            if refC14nPfxSet:
419                # Exclusive C14N requires inclusive namespace elements
420                inclNamespacesElem = transformElem.createAppendElement(\
421                                                                                   refC14nAlg,
422                                                       'InclusiveNamespaces')
423                inclNamespacesElem.node.setAttribute('PrefixList',
424                                        ' '.join(self.refC14nKw['inclusive_namespaces']))
425           
426            # Digest Method
427            digestMethodElem = refElem.createAppendElement(DSIG.BASE, 
428                                                           'DigestMethod')
429            digestMethodElem.node.setAttribute('Algorithm', DSIG.DIGEST_SHA1)
430           
431            # Digest Value
432            digestValueElem = refElem.createAppendElement(DSIG.BASE, 
433                                                          'DigestValue')
434            digestValueElem.createAppendTextNode(digestValue)
435
436   
437        # 2) Signature Generation
438        #       
439        # Canonicalize the signedInfo node
440        docNode = Reader().fromString(str(soapWriter))
441        ctxt = Context(docNode, processorNss=processorNss)
442        signedInfoNode = xpath.Evaluate('//ds:SignedInfo', 
443                                        contextNode=docNode, 
444                                        context=ctxt)[0]
445
446        signedInfoSubsetList = getChildNodes(signedInfoNode)
447       
448        inclusiveNSKWs = self.createUnsupressedPrefixKW(self.signedInfoC14nKw)
449        c14nSignedInfo = Canonicalize(docNode, 
450                                      None, 
451                                      subset=signedInfoSubsetList,
452                                      **inclusiveNSKWs)
453
454        # Calculate digest of SignedInfo
455        signedInfoDigestValue = sha(c14nSignedInfo).digest()
456       
457        # Sign using the private key and base 64 encode the result
458        signatureValue = self.signingPriKey.sign(signedInfoDigestValue)
459        b64EncSignatureValue = base64.encodestring(signatureValue).strip()
460
461        # Add to <SignatureValue>
462        signatureValueElem.createAppendTextNode(b64EncSignatureValue)
463
464        log.info("Signature generation complete")
465
466
467    def createUnsupressedPrefixKW(self, dictToConvert):
468        """
469        Convert a dictionary to use keys with names, 'inclusive_namespaces' in
470        place of keys with names 'unsupressedPrefixes'
471        NB, this is required for the ZSI canonicalize method
472        @type dictToConvert: dict
473        @param dictToConvert: dictionary to convert
474        @rtype: dict
475        @return: dictionary with corrected keys
476        """
477        nsList = []
478        newDict = dictToConvert.copy()
479        if isinstance(newDict, dict) and \
480            isinstance(newDict.get('inclusive_namespaces'), list):
481            nsList = newDict.get('inclusive_namespaces')
482            newDict.pop('inclusive_namespaces')
483
484        newDict['unsuppressedPrefixes'] = nsList
485        return newDict
486
487    def verify(self, parsedSOAP, raiseNoSignatureFound=True):
488        """Verify signature
489       
490        @type parsedSOAP: ZSI.parse.ParsedSoap
491        @param parsedSOAP: object contain parsed SOAP message received from
492        sender"""
493
494        processorNss = \
495        {
496            'ds':     DSIG.BASE, 
497            'wsu':    _WSU.UTILITY, 
498            'wsse':   OASIS.WSSE, 
499            'soapenv':"http://schemas.xmlsoap.org/soap/envelope/" 
500        }
501        ctxt = Context(parsedSOAP.dom, processorNss=processorNss)
502       
503
504        signatureNodes = xpath.Evaluate('//ds:Signature', 
505                                        contextNode=parsedSOAP.dom, 
506                                        context=ctxt)
507        if len(signatureNodes) > 1:
508            raise VerifyError, 'Multiple ds:Signature elements found'
509       
510        try:
511            signatureNodes = signatureNodes[0]
512        except:
513            # Message wasn't signed
514            msg = "Input message wasn't signed!"
515            if raiseNoSignatureFound:
516                raise NoSignatureFound(msg)
517            else: 
518                log.warning(msg)
519                return
520       
521        # Two stage process: reference validation followed by signature
522        # validation
523       
524        # 1) Reference Validation
525       
526        # Check for canonicalization set via ds:CanonicalizationMethod -
527        # Use this later as a back up in case no Canonicalization was set in
528        # the transforms elements
529        c14nMethodNode = xpath.Evaluate('//ds:CanonicalizationMethod', 
530                                        contextNode=parsedSOAP.dom, 
531                                        context=ctxt)[0]
532       
533        refNodes = xpath.Evaluate('//ds:Reference', 
534                                  contextNode=parsedSOAP.dom, 
535                                  context=ctxt)
536
537        for refNode in refNodes:
538            # Get the URI for the reference
539            refURI = refNode.getAttributeNode('URI').value
540                        # skip checking of binary token - since this cannot be
541                        # included in the message if using a Java client with Rampart1.3
542            if refURI == "binaryToken":
543                continue
544                         
545            try:
546                transformsNode = getElements(refNode, "Transforms")[0]
547                transforms = getElements(transformsNode, "Transform")
548   
549                refAlgorithm=transforms[0].getAttributeNode("Algorithm").value
550            except Exception, e:
551                raise VerifyError('failed to get transform algorithm for '
552                                  '<ds:Reference URI="%s">' % \
553                                  (refURI, str(e)))
554               
555            # Add extra keyword for Exclusive canonicalization method
556            refC14nKw = {}
557            if refAlgorithm == DSIG.C14N_EXCL:
558                try:
559                    # Check for no inclusive namespaces set
560                    inclusiveNS = getElements(transforms[0], 
561                                              "InclusiveNamespaces")                   
562                    if inclusiveNS:
563                        pfxListAttNode = \
564                                inclusiveNS[0].getAttributeNode('PrefixList')
565                           
566                        refC14nKw['unsuppressedPrefixes'] = \
567                                                pfxListAttNode.value.split()
568                    else:
569                        # Set to empty list to ensure Exclusive C14N is set for
570                        # Canonicalize call
571                        refC14nKw['unsuppressedPrefixes'] = []
572                except Exception, e:
573                    raise VerifyError('failed to handle transform (%s) in '
574                                      '<ds:Reference URI="%s">: %s' % \
575                                      (transforms[0], refURI, e))
576       
577            # Canonicalize the reference data and calculate the digest
578            if refURI[0] != "#":
579                raise VerifyError("Expecting # identifier for Reference URI "
580                                  "\"%s\"" % refURI)
581                   
582            # XPath reference
583            uriXPath = '//*[@wsu:Id="%s"]' % refURI[1:]
584            uriNode = xpath.Evaluate(uriXPath, 
585                                     contextNode=parsedSOAP.dom, 
586                                     context=ctxt)[0]
587
588            refSubsetList = getChildNodes(uriNode)
589            refC14n = Canonicalize(parsedSOAP.dom,
590                                   None, 
591                                   subset=refSubsetList,
592                                   **refC14nKw)
593            digestValue = base64.encodestring(sha(refC14n).digest()).strip()
594           
595            # Extract the digest value that was stored           
596            digestNode = getElements(refNode, "DigestValue")[0]
597            nodeDigestValue = str(digestNode.childNodes[0].nodeValue).strip()   
598           
599            # Reference validates if the two digest values are the same
600            if digestValue != nodeDigestValue:
601                raise InvalidSignature('Digest Values do not match for URI: '
602                                       '"%s"' % refURI)
603           
604            log.info("Verified canonicalization for element %s" % refURI[1:])
605               
606        # 2) Signature Validation
607        signedInfoNode = xpath.Evaluate('//ds:SignedInfo',
608                                        contextNode=parsedSOAP.dom, 
609                                        context=ctxt)[0]
610
611        # Get algorithm used for canonicalization of the SignedInfo
612        # element.  Nb. This is NOT necessarily the same as that used to
613        # canonicalize the reference elements checked above!
614        signedInfoC14nAlg = c14nMethodNode.getAttributeNode("Algorithm").value
615        signedInfoC14nKw = {}
616        if signedInfoC14nAlg == DSIG.C14N_EXCL:
617            try:
618                # Check for inclusive namespaces
619                inclusiveNS = c14nMethodNode.getElementsByTagName(
620                                                        "InclusiveNamespaces")
621                if inclusiveNS:                   
622                    pfxListAttNode = inclusiveNS[0].getAttributeNode(\
623                                                                 'PrefixList')
624                    signedInfoC14nKw['unsuppressedPrefixes'] = \
625                                                pfxListAttNode.value.split()
626                else:
627                    # Must default to [] otherwise exclusive C14N is not
628                    # triggered
629                    signedInfoC14nKw['unsuppressedPrefixes'] = []
630            except Exception, e:
631                raise VerifyError('failed to handle exclusive '
632                                  'canonicalisation for SignedInfo: %s' % e)
633
634        # Canonicalize the SignedInfo node and take digest
635        signedInfoSubsetList = getChildNodes(signedInfoNode)
636        c14nSignedInfo = Canonicalize(parsedSOAP.dom, 
637                                      None, 
638                                      subset=signedInfoSubsetList,
639                                      **signedInfoC14nKw)
640                             
641        signedInfoDigestValue = sha(c14nSignedInfo).digest()
642       
643        # Get the signature value in order to check against the digest just
644        # calculated
645        signatureValueNode = xpath.Evaluate('//ds:SignatureValue',
646                                            contextNode=parsedSOAP.dom, 
647                                            context=ctxt)[0]
648
649        # Remove base 64 encoding
650        # This line necessary? - only decode call needed??  pyGridWare vers
651        # seems to preserve whitespace
652#        b64EncSignatureValue = \
653#                    str(signatureValueNode.childNodes[0].nodeValue).strip()
654        b64EncSignatureValue = signatureValueNode.childNodes[0].nodeValue
655        signatureValue = base64.decodestring(b64EncSignatureValue)
656
657        # Cache Signature Value here so that a response can include it.
658        #
659        # Nb. If the sign method is called from a separate SignatureHandler
660        # object then the signature value must be passed from THIS object to
661        # the other SignatureHandler otherwise signature confirmation will
662        # fail
663        if self.applySignatureConfirmation:
664            # re-encode string to avoid possible problems with interpretation
665            # of line breaks
666            self.b64EncSignatureValue = b64EncSignatureValue
667        else:
668            self.b64EncSignatureValue = None
669         
670        # Look for X.509 Cert in wsse:BinarySecurityToken node
671        try:
672            binSecTokNode = xpath.Evaluate('//wsse:BinarySecurityToken',
673                                           contextNode=parsedSOAP.dom,
674                                           context=ctxt)[0]
675        except:
676            # Signature may not have included the Binary Security Token in
677            # which case the verifying cert will need to have been set
678            # elsewhere
679            log.info("No Binary Security Token found in WS-Security header")
680            binSecTokNode = None
681       
682        if binSecTokNode:
683            try:
684                x509CertTxt=str(binSecTokNode.childNodes[0].nodeValue)
685               
686                valueType = binSecTokNode.getAttributeNode("ValueType").value
687                if valueType in (self.__class__.binSecTokValType['X509v3'],
688                                 self.__class__.binSecTokValType['X509']):
689                    # Remove base 64 encoding
690                    derString = base64.decodestring(x509CertTxt)
691                    self.verifyingCert = X509Cert.Parse(derString, 
692                                                    format=X509Cert.formatDER)
693                    x509Stack = X509Stack()
694
695                elif valueType == \
696                    self.__class__.binSecTokValType['X509PKIPathv1']:
697                   
698                    derString = base64.decodestring(x509CertTxt)
699                    x509Stack = X509StackParseFromDER(derString)
700                   
701                    # TODO: Check ordering - is the last off the stack the
702                    # one to use to verify the message?
703                    self.verifyingCert = x509Stack[-1]
704                else:
705                    raise WSSecurityError("BinarySecurityToken ValueType "
706                                          'attribute is not recognised: "%s"' %
707                                          valueType)
708                               
709            except Exception, e:
710                raise VerifyError("Error extracting BinarySecurityToken "
711                                  "from WSSE header: %s" % e)
712
713        if self.verifyingCert is None:
714            raise VerifyError("No certificate set for verification of the "
715                              "signature")
716       
717        # Extract RSA public key from the cert
718        rsaPubKey = self.verifyingCert.pubKey.get_rsa()
719
720        # Apply the signature verification
721        try:
722            verify = rsaPubKey.verify(signedInfoDigestValue, signatureValue)
723        except RSA.RSAError, e:
724            raise VerifyError("Error in Signature: " % e)
725       
726        if not verify:
727            raise InvalidSignature("Invalid signature")
728       
729        # Verify chain of trust
730        x509Stack.verifyCertChain(x509Cert2Verify=self.verifyingCert,
731                                  caX509Stack=self._caX509Stack)
732       
733        self._verifyTimeStamp(parsedSOAP, ctxt) 
734        log.info("Signature OK")       
735       
736
737class EncryptionError(WSSecurityError):
738    """Flags an error in the encryption process"""
739
740class DecryptionError(WSSecurityError):
741    """Raised from EncryptionHandler.decrypt if an error occurs with the
742    decryption process"""
743
744
745class EncryptionHandler(object):
746    """Encrypt/Decrypt SOAP messages using WS-Security""" 
747   
748    # Map namespace URIs to Crypto algorithm module and mode
749    cryptoAlg = \
750    {
751         _ENCRYPTION.WRAP_AES256:      {'module':       AES, 
752                                        'mode':         AES.MODE_ECB,
753                                        'blockSize':    16},
754         
755         # CBC (Cipher Block Chaining) modes
756         _ENCRYPTION.BLOCK_AES256:     {'module':       AES, 
757                                        'mode':         AES.MODE_CBC,
758                                        'blockSize':    16},
759                                       
760         _ENCRYPTION.BLOCK_TRIPLEDES:  {'module':       DES3, 
761                                        'mode':         DES3.MODE_CBC,
762                                        'blockSize':    8}   
763    }
764
765     
766    def __init__(self,
767                 signingCertFilePath=None, 
768                 signingPriKeyFilePath=None, 
769                 signingPriKeyPwd=None,
770                 chkSecurityTokRef=False,
771                 encrNS=_ENCRYPTION.BLOCK_AES256):
772       
773        self.__signingCertFilePath = signingCertFilePath
774        self.__signingPriKeyFilePath = signingPriKeyFilePath
775        self.__signingPriKeyPwd = signingPriKeyPwd
776       
777        self.__chkSecurityTokRef = chkSecurityTokRef
778       
779        # Algorithm for shared key encryption
780        try:
781            self.__encrAlg = self.cryptoAlg[encrNS]
782           
783        except KeyError:
784            raise EncryptionError, \
785        'Input encryption algorithm namespace "%s" is not supported' % encrNS
786
787        self.__encrNS = encrNS
788       
789       
790    def encrypt(self, soapWriter):
791        """Encrypt an outbound SOAP message
792       
793        Use Key Wrapping - message is encrypted using a shared key which
794        itself is encrypted with the public key provided by the X.509 cert.
795        signingCertFilePath"""
796       
797        # Use X.509 Cert to encrypt
798        x509Cert = X509.load_cert(self.__signingCertFilePath)
799       
800        soapWriter.dom.setNamespaceAttribute('wsse', OASIS.WSSE)
801        soapWriter.dom.setNamespaceAttribute('xenc', _ENCRYPTION.BASE)
802        soapWriter.dom.setNamespaceAttribute('ds', DSIG.BASE)
803       
804        # TODO: Put in a check to make sure <wsse:security> isn't already
805        # present in header
806        wsseElem = soapWriter._header.createAppendElement(OASIS.WSSE, 
807                                                         'Security')
808        wsseElem.node.setAttribute('SOAP-ENV:mustUnderstand', "1")
809       
810        encrKeyElem = wsseElem.createAppendElement(_ENCRYPTION.BASE, 
811                                                   'EncryptedKey')
812       
813        # Encryption method used to encrypt the shared key
814        keyEncrMethodElem = encrKeyElem.createAppendElement(_ENCRYPTION.BASE, 
815                                                        'EncryptionMethod')
816       
817        keyEncrMethodElem.node.setAttribute('Algorithm', 
818                                            _ENCRYPTION.KT_RSA_1_5)
819
820
821        # Key Info
822        KeyInfoElem = encrKeyElem.createAppendElement(DSIG.BASE, 'KeyInfo')
823       
824        secTokRefElem = KeyInfoElem.createAppendElement(OASIS.WSSE, 
825                                                  'SecurityTokenReference')
826       
827        x509IssSerialElem = secTokRefElem.createAppendElement(DSIG.BASE, 
828                                                          'X509IssuerSerial')
829
830       
831        x509IssNameElem = x509IssSerialElem.createAppendElement(DSIG.BASE, 
832                                                          'X509IssuerName')
833        x509IssNameElem.createAppendTextNode(x509Cert.get_issuer().as_text())
834
835       
836        x509IssSerialNumElem = x509IssSerialElem.createAppendElement(
837                                                  DSIG.BASE, 
838                                                  'X509IssuerSerialNumber')
839       
840        x509IssSerialNumElem.createAppendTextNode(
841                                          str(x509Cert.get_serial_number()))
842
843        # References to what has been encrypted
844        encrKeyCiphDataElem = encrKeyElem.createAppendElement(
845                                                          _ENCRYPTION.BASE,
846                                                          'CipherData')
847       
848        encrKeyCiphValElem = encrKeyCiphDataElem.createAppendElement(
849                                                          _ENCRYPTION.BASE,
850                                                          'CipherValue')
851
852        # References to what has been encrypted
853        refListElem = encrKeyElem.createAppendElement(_ENCRYPTION.BASE,
854                                                      'ReferenceList')
855       
856        dataRefElem = refListElem.createAppendElement(_ENCRYPTION.BASE,
857                                                      'DataReference')
858        dataRefElem.node.setAttribute('URI', "#encrypted")
859
860                     
861        # Add Encrypted data to SOAP body
862        encrDataElem = soapWriter.body.createAppendElement(_ENCRYPTION.BASE, 
863                                                           'EncryptedData')
864        encrDataElem.node.setAttribute('Id', 'encrypted')
865        encrDataElem.node.setAttribute('Type', _ENCRYPTION.BASE) 
866             
867        # Encryption method used to encrypt the target data
868        dataEncrMethodElem = encrDataElem.createAppendElement(
869                                                      _ENCRYPTION.BASE, 
870                                                      'EncryptionMethod')
871       
872        dataEncrMethodElem.node.setAttribute('Algorithm', self.__encrNS)
873       
874        # Cipher data
875        ciphDataElem = encrDataElem.createAppendElement(_ENCRYPTION.BASE,
876                                                        'CipherData')
877       
878        ciphValueElem = ciphDataElem.createAppendElement(_ENCRYPTION.BASE,
879                                                         'CipherValue')
880
881
882        # Get elements from SOAP body for encryption
883        dataElem = soapWriter.body.node.childNodes[0]
884        data = dataElem.toxml()
885     
886        # Pad data to nearest multiple of encryption algorithm's block size   
887        modData = len(data) % self.__encrAlg['blockSize']
888        nPad = modData and self.__encrAlg['blockSize'] - modData or 0
889       
890        # PAd with random junk but ...
891        data += os.urandom(nPad-1)
892       
893        # Last byte should be number of padding bytes
894        # (http://www.w3.org/TR/xmlenc-core/#sec-Alg-Block)
895        data += chr(nPad)       
896       
897        # Generate shared key and input vector - for testing use hard-coded
898        # values to allow later comparison             
899        sharedKey = os.urandom(self.__encrAlg['blockSize'])
900        iv = os.urandom(self.__encrAlg['blockSize'])
901       
902        alg = self.__encrAlg['module'].new(sharedKey,
903                                           self.__encrAlg['mode'],
904                                           iv)
905 
906        # Encrypt required elements - prepend input vector
907        encryptedData = alg.encrypt(iv + data)
908        dataCiphValue = base64.encodestring(encryptedData).strip()
909
910        ciphValueElem.createAppendTextNode(dataCiphValue)
911       
912       
913        # ! Delete unencrypted message body elements !
914        soapWriter.body.node.removeChild(dataElem)
915
916       
917        # Use X.509 cert public key to encrypt the shared key - Extract key
918        # from the cert
919        rsaPubKey = x509Cert.get_pubkey().get_rsa()
920       
921        # Encrypt the shared key
922        encryptedSharedKey = rsaPubKey.public_encrypt(sharedKey, 
923                                                      RSA.pkcs1_padding)
924       
925        encrKeyCiphVal = base64.encodestring(encryptedSharedKey).strip()
926       
927        # Add the encrypted shared key to the EncryptedKey section in the SOAP
928        # header
929        encrKeyCiphValElem.createAppendTextNode(encrKeyCiphVal)
930
931#        print soapWriter.dom.node.toprettyxml()
932#        import pdb;pdb.set_trace()
933       
934       
935    def decrypt(self, parsedSOAP):
936        """Decrypt an inbound SOAP message"""
937       
938        processorNss = \
939        {
940            'xenc':   _ENCRYPTION.BASE,
941            'ds':     DSIG.BASE, 
942            'wsu':    _WSU.UTILITY, 
943            'wsse':   OASIS.WSSE, 
944            'soapenv':"http://schemas.xmlsoap.org/soap/envelope/" 
945        }
946        ctxt = Context(parsedSOAP.dom, processorNss=processorNss)
947       
948        refListNodes = xpath.Evaluate('//xenc:ReferenceList', 
949                                      contextNode=parsedSOAP.dom, 
950                                      context=ctxt)
951        if len(refListNodes) > 1:
952            raise DecryptionError, 'Expecting a single ReferenceList element'
953       
954        try:
955            refListNode = refListNodes[0]
956        except:
957            # Message wasn't encrypted - is this OK or is a check needed for
958            # encryption info in SOAP body - enveloped form?
959            return
960
961
962        # Check for wrapped key encryption
963        encrKeyNodes = xpath.Evaluate('//xenc:EncryptedKey', 
964                                      contextNode=parsedSOAP.dom, 
965                                      context=ctxt)
966        if len(encrKeyNodes) > 1:
967            raise DecryptionError, 'This implementation can only handle ' + \
968                                   'single EncryptedKey element'
969       
970        try:
971            encrKeyNode = encrKeyNodes[0]
972        except:
973            # Shared key encryption used - leave out for the moment
974            raise DecryptionError, 'This implementation can only handle ' + \
975                                   'wrapped key encryption'
976
977       
978        # Check encryption method
979        keyEncrMethodNode = getElements(encrKeyNode, 'EncryptionMethod')[0]     
980        keyAlgorithm = keyEncrMethodNode.getAttributeNode("Algorithm").value
981        if keyAlgorithm != _ENCRYPTION.KT_RSA_1_5:
982            raise DecryptionError, \
983            'Encryption algorithm for wrapped key is "%s", expecting "%s"' % \
984                (keyAlgorithm, _ENCRYPTION.KT_RSA_1_5)
985
986                                                           
987        if self.__chkSecurityTokRef and self.__signingCertFilePath:
988             
989            # Check input cert. against SecurityTokenReference
990            securityTokRefXPath = '/ds:KeyInfo/wsse:SecurityTokenReference'
991            securityTokRefNode = xpath.Evaluate(securityTokRefXPath, 
992                                                contextNode=encrKeyNode, 
993                                                context=ctxt)
994            # TODO: Look for ds:X509* elements to check against X.509 cert
995            # input
996
997
998        # Look for cipher data for wrapped key
999        keyCiphDataNode = getElements(encrKeyNode, 'CipherData')[0]
1000        keyCiphValNode = getElements(keyCiphDataNode, 'CipherValue')[0]
1001
1002        keyCiphVal = str(keyCiphValNode.childNodes[0].nodeValue)
1003        encryptedKey = base64.decodestring(keyCiphVal)
1004
1005        # Read RSA Private key in order to decrypt wrapped key 
1006        priKeyFile = BIO.File(open(self.__signingPriKeyFilePath))         
1007        pwdCallback = lambda *ar, **kw: self.__signingPriKeyPwd                                       
1008        priKey = RSA.load_key_bio(priKeyFile, callback=pwdCallback)
1009       
1010        sharedKey = priKey.private_decrypt(encryptedKey, RSA.pkcs1_padding)
1011       
1012
1013        # Check list of data elements that have been encrypted
1014        for dataRefNode in refListNode.childNodes:
1015
1016            # Get the URI for the reference
1017            dataRefURI = dataRefNode.getAttributeNode('URI').value                           
1018            if dataRefURI[0] != "#":
1019                raise VerifyError, \
1020                    "Expecting # identifier for DataReference URI \"%s\"" % \
1021                    dataRefURI
1022
1023            # XPath reference - need to check for wsu namespace qualified?
1024            #encrNodeXPath = '//*[@wsu:Id="%s"]' % dataRefURI[1:]
1025            encrNodeXPath = '//*[@Id="%s"]' % dataRefURI[1:]
1026            encrNode = xpath.Evaluate(encrNodeXPath, 
1027                                      contextNode=parsedSOAP.dom, 
1028                                      context=ctxt)[0]
1029               
1030            dataEncrMethodNode = getElements(encrNode, 'EncryptionMethod')[0]     
1031            dataAlgorithm = \
1032                        dataEncrMethodNode.getAttributeNode("Algorithm").value
1033            try:       
1034                # Match algorithm name to Crypto module
1035                CryptoAlg = self.cryptoAlg[dataAlgorithm]
1036               
1037            except KeyError:
1038                raise DecryptionError, \
1039'Encryption algorithm for data is "%s", supported algorithms are:\n "%s"' % \
1040                    (keyAlgorithm, "\n".join(self.cryptoAlg.keys()))
1041
1042            # Get Data
1043            dataCiphDataNode = getElements(encrNode, 'CipherData')[0]
1044            dataCiphValNode = getElements(dataCiphDataNode, 'CipherValue')[0]
1045       
1046            dataCiphVal = str(dataCiphValNode.childNodes[0].nodeValue)
1047            encryptedData = base64.decodestring(dataCiphVal)
1048           
1049            alg = CryptoAlg['module'].new(sharedKey, CryptoAlg['mode'])
1050            decryptedData = alg.decrypt(encryptedData)
1051           
1052            # Strip prefix - assume is block size
1053            decryptedData = decryptedData[CryptoAlg['blockSize']:]
1054           
1055            # Strip any padding suffix - Last byte should be number of padding
1056            # bytes
1057            # (http://www.w3.org/TR/xmlenc-core/#sec-Alg-Block)
1058            lastChar = decryptedData[-1]
1059            nPad = ord(lastChar)
1060           
1061            # Sanity check - there may be no padding at all - the last byte
1062            # being the end of the encrypted XML?
1063            #
1064            # TODO: are there better sanity checks than this?!
1065            if nPad < CryptoAlg['blockSize'] and nPad > 0 and \
1066               lastChar != '\n' and lastChar != '>':
1067               
1068                # Follow http://www.w3.org/TR/xmlenc-core/#sec-Alg-Block -
1069                # last byte gives number of padding bytes
1070                decryptedData = decryptedData[:-nPad]
1071
1072
1073            # Parse the encrypted data - inherit from Reader as a fudge to
1074            # enable relevant namespaces to be added prior to parse
1075            processorNss.update({'xsi': SCHEMA.XSI3, 'ns1': 'urn:ZSI:examples'})
1076            class _Reader(Reader):
1077                def initState(self, ownerDoc=None):
1078                    Reader.initState(self, ownerDoc=ownerDoc)
1079                    self._namespaces.update(processorNss)
1080                   
1081            rdr = _Reader()
1082            dataNode = rdr.fromString(decryptedData, ownerDoc=parsedSOAP.dom)
1083           
1084            # Add decrypted element to parent and remove encrypted one
1085            parentNode = encrNode._get_parentNode()
1086            parentNode.appendChild(dataNode)
1087            parentNode.removeChild(encrNode)
1088           
1089            from xml.dom.ext import ReleaseNode
1090            ReleaseNode(encrNode)
1091           
1092            # Ensure body_root attribute is up to date in case it was
1093            # previously encrypted
1094            parsedSOAP.body_root = parsedSOAP.body.childNodes[0]
1095            #print decryptedData
1096            #import pdb;pdb.set_trace()
1097
1098
1099       
1100def getElements(node, nameList):
1101    '''DOM Helper function for getting child elements from a given node'''
1102    # Avoid sub-string matches
1103    nameList = isinstance(nameList, basestring) and [nameList] or nameList
1104    return [n for n in node.childNodes if str(n.localName) in nameList]
1105
1106
1107def getChildNodes(node, nodeList=None):
1108    if nodeList is None:
1109        nodeList = [node] 
1110    return _getChildNodes(node, nodeList)
1111           
1112def _getChildNodes(node, nodeList):
1113
1114    if node.attributes is not None:
1115        nodeList += node.attributes.values() 
1116    nodeList += node.childNodes
1117    for childNode in node.childNodes:
1118        _getChildNodes(childNode, nodeList)
1119    return nodeList
Note: See TracBrowser for help on using the repository browser.