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

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