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

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

Working unit tests for WSGI based Attribute Authority.

  • Altered so that all Attribute Config is picked up from the Paste ini file. Separate cfg or xml based config file is still supported.

TODO:

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