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

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg/TI12-security/trunk/python/ndg.security.common/ndg/security/common/wssecurity/encryptionhandler/dom.py@5357
Revision 5357, 15.7 KB checked in by pjkersha, 11 years ago (diff)
  • fix to WS-Security signature handler 4Suite implementation (ndg.security.common.wssecurity.signaturehandler.foursuite) to ensure timestamp is checked correctly
  • refactored ndg.security.common.wssecurity moving encryption handler development code into its own ndg.security.common.wssecurity.encryptionhandler package
  • Fixed copyright on some remaining files that still had NERC/CCLRC
  • further work on SSL CLient AuthN WSGI unit tests ndg.security.test.unit.wsgi.ssl
Line 
1"""DOM based WS-Security Encryption Handler
2
3NOT COMPLETE OR TESTED
4
5NERC DataGrid Project
6"""
7__author__ = "P J Kershaw"
8__date__ = "05/06/2009"
9__copyright__ = "(C) 2009 Science and Technology Facilities Council"
10__license__ = "BSD - see LICENSE file in top-level directory"
11__contact__ = "Philip.Kershaw@stfc.ac.uk"
12__revision__ = '$Id: $'
13
14import logging
15log = logging.getLogger(__name__)
16from ndg.security.common.wssecurity import WSSecurityError
17
18class EncryptionError(WSSecurityError):
19    """Flags an error in the encryption process"""
20
21class DecryptionError(WSSecurityError):
22    """Raised from EncryptionHandler.decrypt if an error occurs with the
23    decryption process"""
24
25
26class EncryptionHandler(object):
27    """Encrypt/Decrypt SOAP messages using WS-Security""" 
28   
29    # Map namespace URIs to Crypto algorithm module and mode
30    cryptoAlg = \
31    {
32         _ENCRYPTION.WRAP_AES256:      {'module':       AES, 
33                                        'mode':         AES.MODE_ECB,
34                                        'blockSize':    16},
35         
36         # CBC (Cipher Block Chaining) modes
37         _ENCRYPTION.BLOCK_AES256:     {'module':       AES, 
38                                        'mode':         AES.MODE_CBC,
39                                        'blockSize':    16},
40                                       
41         _ENCRYPTION.BLOCK_TRIPLEDES:  {'module':       DES3, 
42                                        'mode':         DES3.MODE_CBC,
43                                        'blockSize':    8}   
44    }
45
46     
47    def __init__(self,
48                 signingCertFilePath=None, 
49                 signingPriKeyFilePath=None, 
50                 signingPriKeyPwd=None,
51                 chkSecurityTokRef=False,
52                 encrNS=_ENCRYPTION.BLOCK_AES256):
53       
54        self.__signingCertFilePath = signingCertFilePath
55        self.__signingPriKeyFilePath = signingPriKeyFilePath
56        self.__signingPriKeyPwd = signingPriKeyPwd
57       
58        self.__chkSecurityTokRef = chkSecurityTokRef
59       
60        # Algorithm for shared key encryption
61        try:
62            self.__encrAlg = self.cryptoAlg[encrNS]
63           
64        except KeyError:
65            raise EncryptionError, \
66        'Input encryption algorithm namespace "%s" is not supported' % encrNS
67
68        self.__encrNS = encrNS
69       
70       
71    def encrypt(self, soapWriter):
72        """Encrypt an outbound SOAP message
73       
74        Use Key Wrapping - message is encrypted using a shared key which
75        itself is encrypted with the public key provided by the X.509 cert.
76        signingCertFilePath"""
77       
78        # Use X.509 Cert to encrypt
79        x509Cert = X509.load_cert(self.__signingCertFilePath)
80       
81        soapWriter.dom.setNamespaceAttribute('wsse', OASIS.WSSE)
82        soapWriter.dom.setNamespaceAttribute('xenc', _ENCRYPTION.BASE)
83        soapWriter.dom.setNamespaceAttribute('ds', DSIG.BASE)
84       
85        # TODO: Put in a check to make sure <wsse:security> isn't already
86        # present in header
87        wsseElem = soapWriter._header.createAppendElement(OASIS.WSSE, 
88                                                         'Security')
89        wsseElem.node.setAttribute('SOAP-ENV:mustUnderstand', "1")
90       
91        encrKeyElem = wsseElem.createAppendElement(_ENCRYPTION.BASE, 
92                                                   'EncryptedKey')
93       
94        # Encryption method used to encrypt the shared key
95        keyEncrMethodElem = encrKeyElem.createAppendElement(_ENCRYPTION.BASE, 
96                                                        'EncryptionMethod')
97       
98        keyEncrMethodElem.node.setAttribute('Algorithm', 
99                                            _ENCRYPTION.KT_RSA_1_5)
100
101
102        # Key Info
103        KeyInfoElem = encrKeyElem.createAppendElement(DSIG.BASE, 'KeyInfo')
104       
105        secTokRefElem = KeyInfoElem.createAppendElement(OASIS.WSSE, 
106                                                  'SecurityTokenReference')
107       
108        x509IssSerialElem = secTokRefElem.createAppendElement(DSIG.BASE, 
109                                                          'X509IssuerSerial')
110
111       
112        x509IssNameElem = x509IssSerialElem.createAppendElement(DSIG.BASE, 
113                                                          'X509IssuerName')
114        x509IssNameElem.createAppendTextNode(x509Cert.get_issuer().as_text())
115
116       
117        x509IssSerialNumElem = x509IssSerialElem.createAppendElement(
118                                                  DSIG.BASE, 
119                                                  'X509IssuerSerialNumber')
120       
121        x509IssSerialNumElem.createAppendTextNode(
122                                          str(x509Cert.get_serial_number()))
123
124        # References to what has been encrypted
125        encrKeyCiphDataElem = encrKeyElem.createAppendElement(
126                                                          _ENCRYPTION.BASE,
127                                                          'CipherData')
128       
129        encrKeyCiphValElem = encrKeyCiphDataElem.createAppendElement(
130                                                          _ENCRYPTION.BASE,
131                                                          'CipherValue')
132
133        # References to what has been encrypted
134        refListElem = encrKeyElem.createAppendElement(_ENCRYPTION.BASE,
135                                                      'ReferenceList')
136       
137        dataRefElem = refListElem.createAppendElement(_ENCRYPTION.BASE,
138                                                      'DataReference')
139        dataRefElem.node.setAttribute('URI', "#encrypted")
140
141                     
142        # Add Encrypted data to SOAP body
143        encrDataElem = soapWriter.body.createAppendElement(_ENCRYPTION.BASE, 
144                                                           'EncryptedData')
145        encrDataElem.node.setAttribute('Id', 'encrypted')
146        encrDataElem.node.setAttribute('Type', _ENCRYPTION.BASE) 
147             
148        # Encryption method used to encrypt the target data
149        dataEncrMethodElem = encrDataElem.createAppendElement(
150                                                      _ENCRYPTION.BASE, 
151                                                      'EncryptionMethod')
152       
153        dataEncrMethodElem.node.setAttribute('Algorithm', self.__encrNS)
154       
155        # Cipher data
156        ciphDataElem = encrDataElem.createAppendElement(_ENCRYPTION.BASE,
157                                                        'CipherData')
158       
159        ciphValueElem = ciphDataElem.createAppendElement(_ENCRYPTION.BASE,
160                                                         'CipherValue')
161
162
163        # Get elements from SOAP body for encryption
164        dataElem = soapWriter.body.node.childNodes[0]
165        data = dataElem.toxml()
166     
167        # Pad data to nearest multiple of encryption algorithm's block size   
168        modData = len(data) % self.__encrAlg['blockSize']
169        nPad = modData and self.__encrAlg['blockSize'] - modData or 0
170       
171        # PAd with random junk but ...
172        data += os.urandom(nPad-1)
173       
174        # Last byte should be number of padding bytes
175        # (http://www.w3.org/TR/xmlenc-core/#sec-Alg-Block)
176        data += chr(nPad)       
177       
178        # Generate shared key and input vector - for testing use hard-coded
179        # values to allow later comparison             
180        sharedKey = os.urandom(self.__encrAlg['blockSize'])
181        iv = os.urandom(self.__encrAlg['blockSize'])
182       
183        alg = self.__encrAlg['module'].new(sharedKey,
184                                           self.__encrAlg['mode'],
185                                           iv)
186 
187        # Encrypt required elements - prepend input vector
188        encryptedData = alg.encrypt(iv + data)
189        dataCiphValue = base64.encodestring(encryptedData).strip()
190
191        ciphValueElem.createAppendTextNode(dataCiphValue)
192       
193       
194        # ! Delete unencrypted message body elements !
195        soapWriter.body.node.removeChild(dataElem)
196
197       
198        # Use X.509 cert public key to encrypt the shared key - Extract key
199        # from the cert
200        rsaPubKey = x509Cert.get_pubkey().get_rsa()
201       
202        # Encrypt the shared key
203        encryptedSharedKey = rsaPubKey.public_encrypt(sharedKey, 
204                                                      RSA.pkcs1_padding)
205       
206        encrKeyCiphVal = base64.encodestring(encryptedSharedKey).strip()
207       
208        # Add the encrypted shared key to the EncryptedKey section in the SOAP
209        # header
210        encrKeyCiphValElem.createAppendTextNode(encrKeyCiphVal)
211
212#        print soapWriter.dom.node.toprettyxml()
213#        import pdb;pdb.set_trace()
214       
215       
216    def decrypt(self, parsedSOAP):
217        """Decrypt an inbound SOAP message"""
218       
219        processorNss = \
220        {
221            'xenc':   _ENCRYPTION.BASE,
222            'ds':     DSIG.BASE, 
223            'wsu':    _WSU.UTILITY, 
224            'wsse':   OASIS.WSSE, 
225            'soapenv':"http://schemas.xmlsoap.org/soap/envelope/" 
226        }
227        ctxt = Context(parsedSOAP.dom, processorNss=processorNss)
228       
229        refListNodes = xpath.Evaluate('//xenc:ReferenceList', 
230                                      contextNode=parsedSOAP.dom, 
231                                      context=ctxt)
232        if len(refListNodes) > 1:
233            raise DecryptionError, 'Expecting a single ReferenceList element'
234       
235        try:
236            refListNode = refListNodes[0]
237        except:
238            # Message wasn't encrypted - is this OK or is a check needed for
239            # encryption info in SOAP body - enveloped form?
240            return
241
242
243        # Check for wrapped key encryption
244        encrKeyNodes = xpath.Evaluate('//xenc:EncryptedKey', 
245                                      contextNode=parsedSOAP.dom, 
246                                      context=ctxt)
247        if len(encrKeyNodes) > 1:
248            raise DecryptionError, 'This implementation can only handle ' + \
249                                   'single EncryptedKey element'
250       
251        try:
252            encrKeyNode = encrKeyNodes[0]
253        except:
254            # Shared key encryption used - leave out for the moment
255            raise DecryptionError, 'This implementation can only handle ' + \
256                                   'wrapped key encryption'
257
258       
259        # Check encryption method
260        keyEncrMethodNode = getElements(encrKeyNode, 'EncryptionMethod')[0]     
261        keyAlgorithm = keyEncrMethodNode.getAttributeNode("Algorithm").value
262        if keyAlgorithm != _ENCRYPTION.KT_RSA_1_5:
263            raise DecryptionError, \
264            'Encryption algorithm for wrapped key is "%s", expecting "%s"' % \
265                (keyAlgorithm, _ENCRYPTION.KT_RSA_1_5)
266
267                                                           
268        if self.__chkSecurityTokRef and self.__signingCertFilePath:
269             
270            # Check input cert. against SecurityTokenReference
271            securityTokRefXPath = '/ds:KeyInfo/wsse:SecurityTokenReference'
272            securityTokRefNode = xpath.Evaluate(securityTokRefXPath, 
273                                                contextNode=encrKeyNode, 
274                                                context=ctxt)
275            # TODO: Look for ds:X509* elements to check against X.509 cert
276            # input
277
278
279        # Look for cipher data for wrapped key
280        keyCiphDataNode = getElements(encrKeyNode, 'CipherData')[0]
281        keyCiphValNode = getElements(keyCiphDataNode, 'CipherValue')[0]
282
283        keyCiphVal = str(keyCiphValNode.childNodes[0].nodeValue)
284        encryptedKey = base64.decodestring(keyCiphVal)
285
286        # Read RSA Private key in order to decrypt wrapped key 
287        priKeyFile = BIO.File(open(self.__signingPriKeyFilePath))         
288        pwdCallback = lambda *ar, **kw: self.__signingPriKeyPwd                                       
289        priKey = RSA.load_key_bio(priKeyFile, callback=pwdCallback)
290       
291        sharedKey = priKey.private_decrypt(encryptedKey, RSA.pkcs1_padding)
292       
293
294        # Check list of data elements that have been encrypted
295        for dataRefNode in refListNode.childNodes:
296
297            # Get the URI for the reference
298            dataRefURI = dataRefNode.getAttributeNode('URI').value                           
299            if dataRefURI[0] != "#":
300                raise VerifyError, \
301                    "Expecting # identifier for DataReference URI \"%s\"" % \
302                    dataRefURI
303
304            # XPath reference - need to check for wsu namespace qualified?
305            #encrNodeXPath = '//*[@wsu:Id="%s"]' % dataRefURI[1:]
306            encrNodeXPath = '//*[@Id="%s"]' % dataRefURI[1:]
307            encrNode = xpath.Evaluate(encrNodeXPath, 
308                                      contextNode=parsedSOAP.dom, 
309                                      context=ctxt)[0]
310               
311            dataEncrMethodNode = getElements(encrNode, 'EncryptionMethod')[0]     
312            dataAlgorithm = \
313                        dataEncrMethodNode.getAttributeNode("Algorithm").value
314            try:       
315                # Match algorithm name to Crypto module
316                CryptoAlg = self.cryptoAlg[dataAlgorithm]
317               
318            except KeyError:
319                raise DecryptionError, \
320'Encryption algorithm for data is "%s", supported algorithms are:\n "%s"' % \
321                    (keyAlgorithm, "\n".join(self.cryptoAlg.keys()))
322
323            # Get Data
324            dataCiphDataNode = getElements(encrNode, 'CipherData')[0]
325            dataCiphValNode = getElements(dataCiphDataNode, 'CipherValue')[0]
326       
327            dataCiphVal = str(dataCiphValNode.childNodes[0].nodeValue)
328            encryptedData = base64.decodestring(dataCiphVal)
329           
330            alg = CryptoAlg['module'].new(sharedKey, CryptoAlg['mode'])
331            decryptedData = alg.decrypt(encryptedData)
332           
333            # Strip prefix - assume is block size
334            decryptedData = decryptedData[CryptoAlg['blockSize']:]
335           
336            # Strip any padding suffix - Last byte should be number of padding
337            # bytes
338            # (http://www.w3.org/TR/xmlenc-core/#sec-Alg-Block)
339            lastChar = decryptedData[-1]
340            nPad = ord(lastChar)
341           
342            # Sanity check - there may be no padding at all - the last byte
343            # being the end of the encrypted XML?
344            #
345            # TODO: are there better sanity checks than this?!
346            if nPad < CryptoAlg['blockSize'] and nPad > 0 and \
347               lastChar != '\n' and lastChar != '>':
348               
349                # Follow http://www.w3.org/TR/xmlenc-core/#sec-Alg-Block -
350                # last byte gives number of padding bytes
351                decryptedData = decryptedData[:-nPad]
352
353
354            # Parse the encrypted data - inherit from Reader as a fudge to
355            # enable relevant namespaces to be added prior to parse
356            processorNss.update({'xsi': SCHEMA.XSI3, 'ns1': 'urn:ZSI:examples'})
357            class _Reader(Reader):
358                def initState(self, ownerDoc=None):
359                    Reader.initState(self, ownerDoc=ownerDoc)
360                    self._namespaces.update(processorNss)
361                   
362            rdr = _Reader()
363            dataNode = rdr.fromString(decryptedData, ownerDoc=parsedSOAP.dom)
364           
365            # Add decrypted element to parent and remove encrypted one
366            parentNode = encrNode._get_parentNode()
367            parentNode.appendChild(dataNode)
368            parentNode.removeChild(encrNode)
369           
370            from xml.dom.ext import ReleaseNode
371            ReleaseNode(encrNode)
372           
373            # Ensure body_root attribute is up to date in case it was
374            # previously encrypted
375            parsedSOAP.body_root = parsedSOAP.body.childNodes[0]
376            #print decryptedData
377            #import pdb;pdb.set_trace()
Note: See TracBrowser for help on using the repository browser.