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

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

Fix checks for inclusive/exclusive namespace use + set default use
of exclusive NS canonicalisation for the dom class - since the ZSI
Canonicalization method does not seem to work without this.

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