source: TI12-security/trunk/NDGSecurity/python/Tests/Echo/wsSecurity.py @ 7080

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg/TI12-security/trunk/NDGSecurity/python/Tests/Echo/wsSecurity.py@7080
Revision 7080, 36.0 KB checked in by pjkersha, 9 years ago (diff)
  • Property svn:executable set to *
  • Property svn:keywords set to Id
Line 
1#!/bin/env python
2
3"""WS-Security test class includes digital signature handler
4
5NERC Data Grid Project
6
7P J Kershaw 01/09/06
8
9Copyright (C) 2009 Science and Technology Facilities Council
10
11"""
12
13__revision__ = '$Id$'
14
15import re
16
17# Digest and signature/verify
18from sha import sha
19from M2Crypto import X509, BIO, RSA
20import base64
21
22# For shared key encryption
23from Crypto.Cipher import AES, DES3
24import os
25
26import ZSI
27from ZSI.wstools.Namespaces import DSIG, ENCRYPTION, OASIS, WSU, WSA200403, \
28                                   SOAP, SCHEMA # last included for xsi
29                                   
30from ZSI.TC import ElementDeclaration,TypeDefinition
31from ZSI.generate.pyclass import pyclass_type
32
33from ZSI.wstools.Utility import DOMException, SplitQName
34from ZSI.wstools.Utility import NamespaceError, MessageInterface, ElementProxy
35
36# XML Parsing
37from cStringIO import StringIO
38from Ft.Xml.Domlette import NonvalidatingReaderBase, NonvalidatingReader
39from Ft.Xml import XPath
40
41# Canonicalization
42from ZSI.wstools.c14n import Canonicalize
43from xml.dom import Node
44from xml.xpath.Context import Context
45from xml import xpath
46
47# Include for re-parsing doc ready for canonicalization in sign method - see
48# associated note
49from xml.dom.ext.reader.PyExpat import Reader
50
51
52class _ENCRYPTION(ENCRYPTION):
53    '''Derived from ENCRYPTION class to add in extra 'tripledes-cbc' - is this
54    any different to 'des-cbc'?  ENCRYPTION class implies that it is the same
55    because it's assigned to 'BLOCK_3DES' ??'''
56    BLOCK_TRIPLEDES = "http://www.w3.org/2001/04/xmlenc#tripledes-cbc"
57
58class _WSU(WSU):
59    '''Try different utility namespace for use with WebSphere'''
60    #UTILITY = "http://schemas.xmlsoap.org/ws/2003/06/utility"
61    UTILITY = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"
62   
63def getElements(node, nameList):
64    '''DOM Helper function for getting child elements from a given node'''
65    # Avoid sub-string matches
66    nameList = isinstance(nameList, basestring) and [nameList] or nameList
67    return [n for n in node.childNodes if str(n.localName) in nameList]
68
69
70class VerifyError(Exception):
71    """Raised from SignatureHandler.verify if signature is invalid"""
72
73class SignatureError(Exception):
74    """Flag if an error occurs during signature generation"""
75       
76class SignatureHandler(object):
77   
78    def __init__(self,
79                 certFilePath=None, 
80                 priKeyFilePath=None, 
81                 priKeyPwd=None):
82       
83        self.__certFilePath = certFilePath
84        self.__priKeyFilePath = priKeyFilePath
85        self.__priKeyPwd = priKeyPwd
86
87
88    def sign(self, soapWriter):
89        '''Sign the message body and binary security token of a SOAP message
90        '''
91        # Add X.509 cert as binary security token
92        x509Cert = X509.load_cert(self.__certFilePath)
93       
94        x509CertPat = re.compile(\
95            '-----BEGIN CERTIFICATE-----\n?(.*?)\n?-----END CERTIFICATE-----',
96            re.S)
97        x509CertStr = x509CertPat.findall(x509Cert.as_pem())[0]
98
99        soapWriter._header.setNamespaceAttribute('wsse', OASIS.WSSE)
100        soapWriter._header.setNamespaceAttribute('wsu', _WSU.UTILITY)
101        soapWriter._header.setNamespaceAttribute('ds', DSIG.BASE)
102        soapWriter._header.setNamespaceAttribute('ec', DSIG.C14N_EXCL)
103       
104        # TODO: Put in a check to make sure <wsse:security> isn't already
105        # present in header
106        wsseElem = soapWriter._header.createAppendElement(OASIS.WSSE, 
107                                                         'Security')
108        wsseElem.setNamespaceAttribute('wsse', OASIS.WSSE)
109        wsseElem.node.setAttribute('SOAP-ENV:mustUnderstand', "1")
110       
111        binSecTokElem = wsseElem.createAppendElement(OASIS.WSSE, 
112                                                     'BinarySecurityToken')
113       
114        # Change value and encoding types to suite WebSphere
115#        binSecTokElem.node.setAttribute('ValueType', "wsse:X509v3")
116        valueType = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509"
117        binSecTokElem.node.setAttribute('ValueType', valueType)
118#        binSecTokElem.node.setAttribute('EncodingType', "wsse:Base64Binary")
119        encodingType = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary"
120        binSecTokElem.node.setAttribute('EncodingType', encodingType)
121       
122        # Add ID so that the binary token can be included in the signature
123        binSecTokElem.node.setAttribute('wsu:Id', "binaryToken")
124
125        binSecTokElem.createAppendTextNode(x509CertStr)
126
127       
128        # Signature
129        signatureElem = wsseElem.createAppendElement(DSIG.BASE, 'Signature')
130        signatureElem.setNamespaceAttribute('ds', DSIG.BASE)
131       
132        # Signature - Signed Info
133        signedInfoElem = signatureElem.createAppendElement(DSIG.BASE, 
134                                                           'SignedInfo')
135       
136        # Signed Info - Canonicalization method
137        signedInfoC14nInclNS = ['xsi', 'xsd', 'SOAP-ENV', 'ds', 'wsse']
138        signedInfoC14nKw = {'unsuppressedPrefixes': signedInfoC14nInclNS} 
139        c14nMethodElem = signedInfoElem.createAppendElement(DSIG.BASE,
140                                                    'CanonicalizationMethod')
141        c14nMethodElem.node.setAttribute('Algorithm', DSIG.C14N_EXCL)
142        c14nInclNamespacesElem = c14nMethodElem.createAppendElement(\
143                                                DSIG.C14N_EXCL,
144                                                'InclusiveNamespaces')
145        c14nInclNamespacesElem.node.setAttribute('PrefixList', 
146                        ' '.join(signedInfoC14nInclNS))
147       
148        # Signed Info - Signature method
149        sigMethodElem = signedInfoElem.createAppendElement(DSIG.BASE,
150                                                    'SignatureMethod')
151        #sigMethodElem.node.setAttribute('Algorithm', DSIG.DIGEST_SHA1)
152        sigMethodElem.node.setAttribute('Algorithm', DSIG.SIG_RSA_SHA1)
153       
154        # Signature - Signature value
155        signatureValueElem = signatureElem.createAppendElement(DSIG.BASE, 
156                                                             'SignatureValue')
157       
158        # Key Info
159        KeyInfoElem = signatureElem.createAppendElement(DSIG.BASE, 'KeyInfo')
160        secTokRefElem = KeyInfoElem.createAppendElement(OASIS.WSSE, 
161                                                  'SecurityTokenReference')
162       
163        # Reference back to the binary token included earlier
164        wsseRefElem = secTokRefElem.createAppendElement(OASIS.WSSE, 
165                                                        'Reference')
166        wsseRefElem.node.setAttribute('URI', "#binaryToken")
167       
168        # Add Reference to body so that it can be included in the signature
169        soapWriter.body.node.setAttribute('wsu:Id', "body")
170        soapWriter.body.node.setAttribute('xmlns:wsu', _WSU.UTILITY)
171
172        # Serialize and re-parse prior to reference generation - calculating
173        # canonicalization based on soapWriter.dom.node seems to give an
174        # error: the order of wsu:Id attribute is not correct
175        docNode = Reader().fromString(str(soapWriter))
176       
177        # Namespaces for XPath searches
178        processorNss = \
179        {
180            'ds':     DSIG.BASE, 
181            'wsu':    _WSU.UTILITY, 
182            'wsse':   OASIS.WSSE, 
183            'soapenv':"http://schemas.xmlsoap.org/soap/envelope/" 
184        }
185        ctxt = Context(docNode, processorNss=processorNss)
186        idNodes = xpath.Evaluate('//*[@wsu:Id]', 
187                                 contextNode=docNode, 
188                                 context=ctxt)
189       
190        # Leave out token
191        #idNodes = [idNodes[1]]
192
193        # 1) Reference Generation
194        #
195        # Find references
196        c14nKw = {}
197        c14nKw['unsuppressedPrefixes'] = ['xmlns', 'xsi', 'xsd', 'SOAP-ENV', 'wsu', 'wsse', 'ns1']
198        for idNode in idNodes:
199           
200            # Set URI attribute to point to reference to be signed
201            #uri = u"#" + idNode.getAttribute('wsu:Id')
202            uri = u"#" + idNode.attributes[(_WSU.UTILITY, 'Id')].value
203           
204            # Canonicalize reference
205            c14nRef = Canonicalize(idNode, **c14nKw)
206           
207            # Calculate digest for reference and base 64 encode
208            #
209            # Nb. encodestring adds a trailing newline char
210            digestValue = base64.encodestring(sha(c14nRef).digest()).strip()
211
212
213            # Add a new reference element to SignedInfo
214            refElem = signedInfoElem.createAppendElement(DSIG.BASE, 
215                                                         'Reference')
216            refElem.node.setAttribute('URI', uri)
217           
218            # Use ds:Transforms or wsse:TransformationParameters?
219            transformsElem = refElem.createAppendElement(DSIG.BASE, 
220                                                        'Transforms')
221            transformElem = transformsElem.createAppendElement(DSIG.BASE, 
222                                                               'Transform')
223#            transformElem.node.setAttribute('Algorithm', DSIG.C14N)
224            transformElem.node.setAttribute('Algorithm', DSIG.C14N_EXCL)
225
226            inclNamespacesElem = transformElem.createAppendElement(\
227                                                        DSIG.C14N_EXCL,
228                                                       'InclusiveNamespaces')
229            inclNamespacesElem.node.setAttribute('PrefixList',
230                                ' '.join(c14nKw['unsuppressedPrefixes']))
231           
232            # Digest Method
233            digestMethodElem = refElem.createAppendElement(DSIG.BASE, 
234                                                           'DigestMethod')
235            digestMethodElem.node.setAttribute('Algorithm', DSIG.DIGEST_SHA1)
236           
237            # Digest Value
238            digestValueElem = refElem.createAppendElement(DSIG.BASE, 
239                                                          'DigestValue')
240            digestValueElem.createAppendTextNode(digestValue)
241
242   
243        # 2) Signature Generation
244        #
245
246        # Test against signature generated by pyXMLSec version
247        #xmlTxt = open('./wsseSign-xmlsec-res.xml').read()
248        #dom = NonvalidatingReader.parseStream(StringIO(xmlTxt))
249       
250        # Canonicalize the signedInfo node
251        #
252        # Nb. When extracted the code adds the namespace attribute to the
253        # signedInfo!  This has important consequences for validation -
254        #
255        # 1) Do you strip the namespace attribute before taking the digest to
256        # ensure the text is exactly the same as what is displayed in the
257        # message?
258        #
259        # 2) Leave it in and assume the validation algorithm will expect to
260        # add in the namespace attribute?!
261        #
262        # http://www.w3.org/TR/xml-c14n#NoNSPrefixRewriting implies you need
263        # to include namespace declarations for namespaces referenced in a doc
264        # subset - yes to 2)
265        #c14nSignedInfo = signedInfoElem.canonicalize()
266        c14nSignedInfo = Canonicalize(signedInfoElem.node, **signedInfoC14nKw)
267
268        # Calculate digest of SignedInfo
269        signedInfoDigestValue = sha(c14nSignedInfo).digest().strip()
270       
271        # Read Private key to sign with   
272        priKeyFile = BIO.File(open(self.__priKeyFilePath))                                           
273        priKey = RSA.load_key_bio(priKeyFile, 
274                                  callback=lambda *ar, **kw: self.__priKeyPwd)
275       
276        # Sign using the private key and base 64 encode the result
277        signatureValue = priKey.sign(signedInfoDigestValue)
278        b64EncSignatureValue = base64.encodestring(signatureValue).strip()
279
280        # Add to <SignatureValue>
281        signatureValueElem.createAppendTextNode(b64EncSignatureValue)
282       
283        # Extract RSA public key from the cert
284        rsaPubKey = x509Cert.get_pubkey().get_rsa()
285       
286        # Check the signature
287#        verify = bool(rsaPubKey.verify(signedInfoDigestValue, signatureValue))
288#       
289#        open('soap.xml', 'w').write(str(soapWriter))
290        import pdb;pdb.set_trace() 
291        print "Signature Generated"
292        print str(soapWriter)
293
294
295    def verify(self, parsedSOAP):
296        """Verify signature"""
297       
298        processorNss = \
299        {
300            'ds':     DSIG.BASE, 
301            'wsu':    _WSU.UTILITY, 
302            'wsse':   OASIS.WSSE, 
303            'soapenv':"http://schemas.xmlsoap.org/soap/envelope/" 
304        }
305        ctxt = Context(parsedSOAP.dom, processorNss=processorNss)
306       
307
308        signatureNodes = xpath.Evaluate('//ds:Signature', 
309                                        contextNode=parsedSOAP.dom, 
310                                        context=ctxt)
311        if len(signatureNodes) > 1:
312            raise VerifyError, 'Multiple ds:Signature elements found'
313       
314        try:
315            signatureNodes = signatureNodes[0]
316        except:
317            # Message wasn't signed
318            return
319       
320        # Two stage process: reference validation followed by signature
321        # validation
322       
323        # 1) Reference Validation
324       
325        # Check for canonicalization set via ds:CanonicalizationMethod -
326        # Use this later as a back up in case no Canonicalization was set in
327        # the transforms elements
328        c14nMethodNode = xpath.Evaluate('//ds:CanonicalizationMethod', 
329                                        contextNode=parsedSOAP.dom, 
330                                        context=ctxt)[0]
331       
332        refNodes = xpath.Evaluate('//ds:Reference', 
333                                  contextNode=parsedSOAP.dom, 
334                                  context=ctxt)
335           
336        for refNode in refNodes:
337            # Get the URI for the reference
338            refURI = refNode.getAttributeNodeNS(None, 'URI').value
339                           
340            try:
341                transformsNode = getElements(refNode, "Transforms")[0]
342                transforms = getElements(transformsNode, "Transform")
343   
344                refAlgorithm = transforms[0].getAttributeNodeNS(None, 
345                                                         "Algorithm").value
346            except Exception, e:
347                raise VerifyError, \
348            'failed to get transform algorithm for <ds:Reference URI="%s">'%\
349                        (refURI, str(e))
350               
351            # Add extra keyword for Exclusive canonicalization method
352            c14nKw = {}
353            if refAlgorithm == DSIG.C14N_EXCL:
354                try:
355                    inclusiveNS = getElements(transforms[0], 
356                                              "InclusiveNamespaces")
357                   
358                    pfxListAttNode = inclusiveNS[0].getAttributeNodeNS(None, 
359                                                               'PrefixList')
360                    c14nKw['unsuppressedPrefixes'] = \
361                                                pfxListAttNode.value.split()
362                except:
363                    raise VerifyError, \
364                'failed to handle transform (%s) in <ds:Reference URI="%s">'%\
365                        (transforms[0], refURI)
366       
367            # Canonicalize the reference data and calculate the digest
368            if refURI[0] != "#":
369                raise VerifyError, \
370                    "Expecting # identifier for Reference URI \"%s\"" % refURI
371                   
372            # XPath reference
373            uriXPath = '//*[@wsu:Id="%s"]' % refURI[1:]
374            uriNode = xpath.Evaluate(uriXPath, 
375                                     contextNode=parsedSOAP.dom, 
376                                     context=ctxt)[0]
377
378            c14nRef = Canonicalize(uriNode, **c14nKw)
379            digestValue = base64.encodestring(sha(c14nRef).digest()).strip()
380           
381            # Extract the digest value that was stored           
382            digestNode = getElements(refNode, "DigestValue")[0]
383            nodeDigestValue = str(digestNode.childNodes[0].nodeValue).strip()   
384           
385            # Reference validates if the two digest values are the same
386            if digestValue != nodeDigestValue:
387                raise VerifyError, \
388                        'Digest Values do not match for URI: "%s"' % refURI
389               
390        # 2) Signature Validation
391        signedInfoNode = xpath.Evaluate('//ds:SignedInfo',
392                                        contextNode=parsedSOAP.dom, 
393                                        context=ctxt)[0]
394
395        #import pdb;pdb.set_trace()
396        # Get algorithm used for canonicalization of the SignedInfo
397        # element.  Nb. This is NOT necessarily the same as that used to
398        # canonicalize the reference elements checked above!
399        signedInfoC14nAlg = c14nMethodNode.getAttributeNodeNS(None, 
400                                                         "Algorithm").value
401        signedInfoC14nKw = {}
402        if signedInfoC14nAlg == DSIG.C14N_EXCL:
403            try:
404                inclusiveNS = getElements(c14nMethodNode,
405                                          "InclusiveNamespaces")
406               
407                pfxListAttNode = inclusiveNS[0].getAttributeNodeNS(None, 
408                                                           'PrefixList')
409                signedInfoC14nKw['unsuppressedPrefixes'] = \
410                                            pfxListAttNode.value.split()
411            except Exception, e:
412                raise VerifyError, \
413            'failed to handle exclusive canonicalisation for SignedInfo: %s'%\
414                        str(e)
415
416        # Canonicalize the SignedInfo node and take digest
417        c14nSignedInfo = Canonicalize(signedInfoNode, **signedInfoC14nKw)       
418        signedInfoDigestValue = sha(c14nSignedInfo).digest()
419       
420        # Get the signature value in order to check against the digest just
421        # calculated
422        signatureValueNode = xpath.Evaluate('//ds:SignatureValue',
423                                            contextNode=parsedSOAP.dom, 
424                                            context=ctxt)[0]
425
426        # Remove base 64 encoding
427        b64EncSignatureValue = \
428                    str(signatureValueNode.childNodes[0].nodeValue).strip()
429                   
430        signatureValue = base64.decodestring(b64EncSignatureValue)
431
432
433        # Look for X.509 Cert in wsse:BinarySecurityToken node
434        try:
435            binSecTokNode = xpath.Evaluate('//wsse:BinarySecurityToken',
436                                           contextNode=parsedSOAP.dom,
437                                           context=ctxt)[0]
438            x509str = binSecTokNode.childNodes[0]._get_nodeValue()
439            #import pdb;pdb.set_trace()
440#            x509strAlt = ''
441#            i = 0
442#            while i < len(x509str):
443#                x509strAlt += "%s\n" % x509str[i:i+64]
444#                i += 64
445   
446            raise Exception, "Try reading from file"
447            x509Cert = X509.load_cert_string(x509strAlt)
448        except:
449            # If not, check cert file   
450            x509Cert = X509.load_cert(self.__certFilePath)
451       
452        # Extract RSA public key from the cert
453        rsaPubKey = x509Cert.get_pubkey().get_rsa()
454       
455        # Apply the signature verification
456        try:
457            verify = bool(rsaPubKey.verify(signedInfoDigestValue, 
458                                           signatureValue))
459        except RSA.RSAError:
460            raise VerifyError, "Error in Signature"
461       
462        if not verify:
463            raise VerifyError, "Invalid signature"
464       
465#        import pdb;pdb.set_trace()
466        print "Signature OK"
467
468
469class EncryptionError(Exception):
470    """Flags an error in the encryption process"""
471
472class DecryptionError(Exception):
473    """Raised from EncryptionHandler.decrypt if an error occurs with the
474    decryption process"""
475
476
477class EncryptionHandler(object):
478    """Encrypt/Decrypt SOAP messages using WS-Security""" 
479   
480    # Map namespace URIs to Crypto algorithm module and mode
481    cryptoAlg = \
482    {
483         _ENCRYPTION.WRAP_AES256:      {'module':       AES, 
484                                        'mode':         AES.MODE_ECB,
485                                        'blockSize':    16},
486         
487         # CBC (Cipher Block Chaining) modes
488         _ENCRYPTION.BLOCK_AES256:     {'module':       AES, 
489                                        'mode':         AES.MODE_CBC,
490                                        'blockSize':    16},
491                                       
492         _ENCRYPTION.BLOCK_TRIPLEDES:  {'module':       DES3, 
493                                        'mode':         DES3.MODE_CBC,
494                                        'blockSize':    8}   
495    }
496
497     
498    def __init__(self,
499                 certFilePath=None, 
500                 priKeyFilePath=None, 
501                 priKeyPwd=None,
502                 chkSecurityTokRef=False,
503                 encrNS=_ENCRYPTION.BLOCK_AES256):
504#                 encrNS=_ENCRYPTION.BLOCK_TRIPLEDES):
505       
506        self.__certFilePath = certFilePath
507        self.__priKeyFilePath = priKeyFilePath
508        self.__priKeyPwd = priKeyPwd
509       
510        self.__chkSecurityTokRef = chkSecurityTokRef
511       
512        # Algorithm for shared key encryption
513        try:
514            self.__encrAlg = self.cryptoAlg[encrNS]
515           
516        except KeyError:
517            raise EncryptionError, \
518        'Input encryption algorithm namespace "%s" is not supported' % encrNS
519
520        self.__encrNS = encrNS
521       
522       
523    def encrypt(self, soapWriter):
524        """Encrypt an outbound SOAP message
525       
526        Use Key Wrapping - message is encrypted using a shared key which
527        itself is encrypted with the public key provided by the X.509 cert.
528        certFilePath"""
529       
530        # Use X.509 Cert to encrypt
531        x509Cert = X509.load_cert(self.__certFilePath)
532       
533        x509CertPat = re.compile(\
534            '-----BEGIN CERTIFICATE-----\n?(.*?)\n?-----END CERTIFICATE-----',
535            re.S)
536        x509CertStr = x509CertPat.findall(x509Cert.as_pem())[0]
537
538
539        soapWriter.dom.setNamespaceAttribute('wsse', OASIS.WSSE)
540        soapWriter.dom.setNamespaceAttribute('xenc', _ENCRYPTION.BASE)
541        soapWriter.dom.setNamespaceAttribute('ds', DSIG.BASE)
542       
543        # TODO: Put in a check to make sure <wsse:security> isn't already
544        # present in header
545        wsseElem = soapWriter._header.createAppendElement(OASIS.WSSE, 
546                                                         'Security')
547        wsseElem.node.setAttribute('SOAP-ENV:mustUnderstand', "1")
548       
549        encrKeyElem = wsseElem.createAppendElement(_ENCRYPTION.BASE, 
550                                                   'EncryptedKey')
551       
552        # Encryption method used to encrypt the shared key
553        keyEncrMethodElem = encrKeyElem.createAppendElement(_ENCRYPTION.BASE, 
554                                                        'EncryptionMethod')
555       
556        keyEncrMethodElem.node.setAttribute('Algorithm', 
557                                            _ENCRYPTION.KT_RSA_1_5)
558
559
560        # Key Info
561        KeyInfoElem = encrKeyElem.createAppendElement(DSIG.BASE, 'KeyInfo')
562       
563        secTokRefElem = KeyInfoElem.createAppendElement(OASIS.WSSE, 
564                                                  'SecurityTokenReference')
565        keyIdElem = secTokRefElem.createAppendElement(OASIS.WSSE,
566                                                      'KeyIdentifier')
567        #import pdb;pdb.set_trace()
568        # Change value and encoding types to suite WebSphere
569        valueType = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509SubjectKeyIdentifier"
570        keyIdElem.node.setAttribute('ValueType', valueType)
571        encodingType = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary"
572        keyIdElem.node.setAttribute('EncodingType', encodingType)
573       
574        # Add ID so that the binary token can be included in the signature
575        #keyIdElem.node.setAttribute('wsu:Id', "binaryToken")
576
577        keyIdElem.createAppendTextNode(x509CertStr)
578       
579#        x509IssSerialElem = secTokRefElem.createAppendElement(DSIG.BASE,
580#                                                          'X509IssuerSerial')
581#
582#       
583#        x509IssNameElem = x509IssSerialElem.createAppendElement(DSIG.BASE,
584#                                                          'X509IssuerName')
585#        x509IssNameElem.createAppendTextNode(x509Cert.get_issuer().as_text())
586#
587#       
588#        x509IssSerialNumElem = x509IssSerialElem.createAppendElement(
589#                                                  DSIG.BASE,
590#                                                  'X509IssuerSerialNumber')
591#       
592#        x509IssSerialNumElem.createAppendTextNode(
593#                                          str(x509Cert.get_serial_number()))
594
595        # References to what has been encrypted
596        encrKeyCiphDataElem = encrKeyElem.createAppendElement(
597                                                          _ENCRYPTION.BASE,
598                                                          'CipherData')
599       
600        encrKeyCiphValElem = encrKeyCiphDataElem.createAppendElement(
601                                                          _ENCRYPTION.BASE,
602                                                          'CipherValue')
603
604        # References to what has been encrypted
605        refListElem = encrKeyElem.createAppendElement(_ENCRYPTION.BASE,
606                                                      'ReferenceList')
607       
608        dataRefElem = refListElem.createAppendElement(_ENCRYPTION.BASE,
609                                                      'DataReference')
610        dataRefElem.node.setAttribute('URI', "#encrypted")
611
612                     
613        # Add Encrypted data to SOAP body
614        encrDataElem = soapWriter.body.createAppendElement(_ENCRYPTION.BASE, 
615                                                           'EncryptedData')
616        encrDataElem.node.setAttribute('Id', 'encrypted')
617        encrDataElem.node.setAttribute('Type', _ENCRYPTION.BASE) 
618             
619        # Encryption method used to encrypt the target data
620        dataEncrMethodElem = encrDataElem.createAppendElement(
621                                                      _ENCRYPTION.BASE, 
622                                                      'EncryptionMethod')
623       
624        dataEncrMethodElem.node.setAttribute('Algorithm', self.__encrNS)
625       
626        # Cipher data
627        ciphDataElem = encrDataElem.createAppendElement(_ENCRYPTION.BASE,
628                                                        'CipherData')
629       
630        ciphValueElem = ciphDataElem.createAppendElement(_ENCRYPTION.BASE,
631                                                         'CipherValue')
632
633
634        # Get elements from SOAP body for encryption
635        dataElem = soapWriter.body.node.childNodes[0]
636        data = dataElem.toxml()
637     
638        # Pad data to nearest multiple of encryption algorithm's block size   
639        modData = len(data) % self.__encrAlg['blockSize']
640        nPad = modData and self.__encrAlg['blockSize'] - modData or 0
641       
642        # PAd with random junk but ...
643        data += os.urandom(nPad-1)
644       
645        # Last byte should be number of padding bytes
646        # (http://www.w3.org/TR/xmlenc-core/#sec-Alg-Block)
647        data += chr(nPad)       
648       
649        # Generate shared key and input vector - for testing use hard-coded
650        # values to allow later comparison             
651        sharedKey = os.urandom(self.__encrAlg['blockSize'])
652        iv = os.urandom(self.__encrAlg['blockSize'])
653        #iv = os.urandom(8)
654       
655        alg = self.__encrAlg['module'].new(sharedKey,
656                                           self.__encrAlg['mode'],
657                                           iv)
658 
659        # Encrypt required elements - prepend input vector
660        encryptedData = alg.encrypt(iv + data)
661        dataCiphValue = base64.encodestring(encryptedData).strip()
662
663        ciphValueElem.createAppendTextNode(dataCiphValue)
664       
665       
666        # ! Delete unencrypted message body elements !
667        soapWriter.body.node.removeChild(dataElem)
668
669       
670        # Use X.509 cert public key to encrypt the shared key - Extract key
671        # from the cert
672        rsaPubKey = x509Cert.get_pubkey().get_rsa()
673       
674        # Encrypt the shared key
675        encryptedSharedKey = rsaPubKey.public_encrypt(sharedKey, 
676                                                      RSA.pkcs1_padding)
677       
678        encrKeyCiphVal = base64.encodestring(encryptedSharedKey).strip()
679       
680        # Add the encrypted shared key to the EncryptedKey section in the SOAP
681        # header
682        encrKeyCiphValElem.createAppendTextNode(encrKeyCiphVal)
683
684        print soapWriter.dom.node.toprettyxml()
685#        import pdb;pdb.set_trace()
686       
687       
688    def decrypt(self, parsedSOAP):
689        """Decrypt an inbound SOAP message"""
690       
691        processorNss = \
692        {
693            'xenc':   _ENCRYPTION.BASE,
694            'ds':     DSIG.BASE, 
695            'wsu':    _WSU.UTILITY, 
696            'wsse':   OASIS.WSSE, 
697            'soapenv':"http://schemas.xmlsoap.org/soap/envelope/" 
698        }
699        ctxt = Context(parsedSOAP.dom, processorNss=processorNss)
700       
701        refListNodes = xpath.Evaluate('//xenc:ReferenceList', 
702                                      contextNode=parsedSOAP.dom, 
703                                      context=ctxt)
704        if len(refListNodes) > 1:
705            raise DecryptionError, 'Expecting a single ReferenceList element'
706       
707        try:
708            refListNode = refListNodes[0]
709        except:
710            # Message wasn't encrypted - is this OK or is a check needed for
711            # encryption info in SOAP body - enveloped form?
712            return
713
714
715        # Check for wrapped key encryption
716        encrKeyNodes = xpath.Evaluate('//xenc:EncryptedKey', 
717                                      contextNode=parsedSOAP.dom, 
718                                      context=ctxt)
719        if len(encrKeyNodes) > 1:
720            raise DecryptionError, 'This implementation can only handle ' + \
721                                   'single EncryptedKey element'
722       
723        try:
724            encrKeyNode = encrKeyNodes[0]
725        except:
726            # Shared key encryption used - leave out for the moment
727            raise DecryptionError, 'This implementation can only handle ' + \
728                                   'wrapped key encryption'
729
730       
731        # Check encryption method
732        keyEncrMethodNode = getElements(encrKeyNode, 'EncryptionMethod')[0]     
733        keyAlgorithm = keyEncrMethodNode.getAttributeNodeNS(None, 
734                                                            "Algorithm").value
735        if keyAlgorithm != _ENCRYPTION.KT_RSA_1_5:
736            raise DecryptionError, \
737            'Encryption algorithm for wrapped key is "%s", expecting "%s"' % \
738                (keyAlgorithm, _ENCRYPTION.KT_RSA_1_5)
739
740                                                           
741        if self.__chkSecurityTokRef and self.__certFilePath:
742             
743            # Check input cert. against SecurityTokenReference
744            securityTokRefXPath = '/ds:KeyInfo/wsse:SecurityTokenReference'
745            securityTokRefNode = xpath.Evaluate(securityTokRefXPath, 
746                                                contextNode=encrKeyNode, 
747                                                context=ctxt)
748            # TODO: Look for ds:X509* elements to check against X.509 cert
749            # input
750
751
752        # Look for cipher data for wrapped key
753        keyCiphDataNode = getElements(encrKeyNode, 'CipherData')[0]
754        keyCiphValNode = getElements(keyCiphDataNode, 'CipherValue')[0]
755
756        keyCiphVal = str(keyCiphValNode.childNodes[0].nodeValue)
757        encryptedKey = base64.decodestring(keyCiphVal)
758
759        # Read RSA Private key in order to decrypt wrapped key 
760        priKeyFile = BIO.File(open(self.__priKeyFilePath))                                           
761        priKey = RSA.load_key_bio(priKeyFile, 
762                                  callback=lambda *ar, **kw: self.__priKeyPwd)
763       
764        sharedKey = priKey.private_decrypt(encryptedKey, RSA.pkcs1_padding)
765       
766
767        # Check list of data elements that have been encrypted
768        for dataRefNode in refListNode.childNodes:
769
770            # Get the URI for the reference
771            dataRefURI = dataRefNode.getAttributeNodeNS(None, 'URI').value                           
772            if dataRefURI[0] != "#":
773                raise VerifyError, \
774                    "Expecting # identifier for DataReference URI \"%s\"" % \
775                    dataRefURI
776
777            # XPath reference - need to check for wsu namespace qualified?
778            #encrNodeXPath = '//*[@wsu:Id="%s"]' % dataRefURI[1:]
779            encrNodeXPath = '//*[@Id="%s"]' % dataRefURI[1:]
780            encrNode = xpath.Evaluate(encrNodeXPath, 
781                                      contextNode=parsedSOAP.dom, 
782                                      context=ctxt)[0]
783               
784            dataEncrMethodNode = getElements(encrNode, 'EncryptionMethod')[0]     
785            dataAlgorithm = dataEncrMethodNode.getAttributeNodeNS(None, 
786                                                            "Algorithm").value
787            try:       
788                # Match algorithm name to Crypto module
789                CryptoAlg = self.cryptoAlg[dataAlgorithm]
790               
791            except KeyError:
792                raise DecryptionError, \
793'Encryption algorithm for data is "%s", supported algorithms are:\n "%s"' % \
794                    (keyAlgorithm, "\n".join(self.cryptoAlg.keys()))
795
796            # Get Data
797            dataCiphDataNode = getElements(encrNode, 'CipherData')[0]
798            dataCiphValNode = getElements(dataCiphDataNode, 'CipherValue')[0]
799       
800            dataCiphVal = str(dataCiphValNode.childNodes[0].nodeValue)
801            encryptedData = base64.decodestring(dataCiphVal)
802           
803            alg = CryptoAlg['module'].new(sharedKey, CryptoAlg['mode'])
804            decryptedData = alg.decrypt(encryptedData)
805           
806            # Strip prefix - assume is block size
807            decryptedData = decryptedData[CryptoAlg['blockSize']:]
808           
809            # Strip any padding suffix - Last byte should be number of padding
810            # bytes
811            # (http://www.w3.org/TR/xmlenc-core/#sec-Alg-Block)
812            lastChar = decryptedData[-1]
813            nPad = ord(lastChar)
814           
815            # Sanity check - there may be no padding at all - the last byte
816            # being the end of the encrypted XML?
817            #
818            # TODO: are there better sanity checks than this?!
819            if nPad < CryptoAlg['blockSize'] and nPad > 0 and \
820               lastChar != '\n' and lastChar != '>':
821               
822                # Follow http://www.w3.org/TR/xmlenc-core/#sec-Alg-Block -
823                # last byte gives number of padding bytes
824                decryptedData = decryptedData[:-nPad]
825
826
827            # Parse the encrypted data - inherit from Reader as a fudge to
828            # enable relevant namespaces to be added prior to parse
829            processorNss.update({'xsi': SCHEMA.XSI3, 'ns1': 'urn:ZSI:examples'})
830            class _Reader(Reader):
831                def initState(self, ownerDoc=None):
832                    Reader.initState(self, ownerDoc=ownerDoc)
833                    self._namespaces.update(processorNss)
834                   
835            rdr = _Reader()
836            dataNode = rdr.fromString(decryptedData, ownerDoc=parsedSOAP.dom)
837           
838            # Add decrypted element to parent and remove encrypted one
839            parentNode = encrNode._get_parentNode()
840            parentNode.appendChild(dataNode)
841            parentNode.removeChild(encrNode)
842           
843            from xml.dom.ext import ReleaseNode
844            ReleaseNode(encrNode)
845           
846            # Ensure body_root attribute is up to date in case it was
847            # previously encrypted
848            parsedSOAP.body_root = parsedSOAP.body.childNodes[0]
849            print decryptedData
850            #import pdb;pdb.set_trace()
851           
852
853
854       
855if __name__ == "__main__":
856    import sys
857    txt = None
858   
859    e = EncryptionHandler(certFilePath='../../Junk-cert.pem',
860                          priKeyFilePath='../../Junk-key.pem',
861                          priKeyPwd=open('../../tmp2').read().strip())
862   
863    encryptedData = e.encrypt(None)
864    print e.decrypt(None, encryptedData)
865   
Note: See TracBrowser for help on using the repository browser.