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

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

General refactoring and updating of code, including:

Removal of refC14nKw and singnedInfoC14nKw keywords in wsssecurity session manager config
(the refC14nInclNS and signedInfoC14nInclNS keywords are sufficient);
Creation of new DOM signature handler class, dom.py, based on the wsSecurity
class;
Abstraction of common code between dom.py and etree.py into new parent
class, BaseSignatureHandler?.py.
Fixing and extending use of properties in the SignatureHandler? code.
Fixing a few bugs with the original SignatureHandler? code.
Updating of test cases to new code/code structure.

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