]> wagner.pp.ru Git - oss/ctypescrypto.git/commitdiff
Fixed processing of encrypted private keys. Added tests for encrypted private keys
authorVictor Wagner <vitus@wagner.pp.ru>
Tue, 15 Nov 2016 21:23:54 +0000 (00:23 +0300)
committerVictor Wagner <vitus@wagner.pp.ru>
Tue, 15 Nov 2016 21:23:54 +0000 (00:23 +0300)
ctypescrypto/pkey.py
tests/testpkey.py

index 59da723f7b93f926a44b2d766c19c00842ea939a..af31a6725bc5ed04726ee7c401bed94e3ee331e6 100644 (file)
@@ -5,33 +5,45 @@ PKey object of this module is wrapper around OpenSSL EVP_PKEY object.
 """
 
 
-from ctypes import c_char_p, c_void_p, c_int, c_long, POINTER
+from ctypes import c_char, c_char_p, c_void_p, c_int, c_long, POINTER
 from ctypes import create_string_buffer, byref, memmove, CFUNCTYPE
 from ctypescrypto import libcrypto
 from ctypescrypto.exception import LibCryptoError, clear_err_stack
 from ctypescrypto.bio import Membio
 
-__all__ = ['PKeyError', 'password_callback', 'PKey', 'PW_CALLBACK_FUNC']
+__all__ = ['PKeyError', 'PKey', 'PW_CALLBACK_FUNC']
 class PKeyError(LibCryptoError):
     """ Exception thrown if libcrypto finctions return an error """
     pass
 
-PW_CALLBACK_FUNC = CFUNCTYPE(c_int, c_char_p, c_int, c_int, c_char_p)
+PW_CALLBACK_FUNC = CFUNCTYPE(c_int, POINTER(c_char), c_int, c_int, c_char_p)
 """ Function type for pem password callback """
 
-def password_callback(buf, length, rwflag, userdata):
+def _password_callback(c):
     """
-    Example password callback for private key. Assumes that
-    password is stored in the userdata parameter, so allows to pass password
-    from constructor arguments to the libcrypto keyloading functions
+    Converts given user function or string to C password callback
+    function, passable to openssl.
+
+    IF function is passed, it would be called upon reading or writing
+    PEM format private key with one argument which is True if we are
+    writing key and should verify passphrase and false if we are reading
+
     """
-    cnt = len(userdata)
-    if length < cnt:
-        cnt = length
-    memmove(buf, userdata, cnt)
-    return cnt
+    if  c is None:
+        return PW_CALLBACK_FUNC(0)
+    if callable(c):
+         def __cb(buf, length, rwflag, userdata):
+             pwd = c(rwflag)
+             cnt = min(len(pwd),length)
+             memmove(buf,pwd, cnt)
+             return cnt
+    else:
+        def __cb(buf,length,rwflag,userdata):
+            cnt=min(len(c),length)
+            memmove(buf,c,cnt)
+            return cnt
+    return PW_CALLBACK_FUNC(__cb)        
 
-_cb = PW_CALLBACK_FUNC(password_callback)
 
 class PKey(object):
     """
@@ -48,7 +60,36 @@ class PKey(object):
          libcrypto routines
     """
     def __init__(self, ptr=None, privkey=None, pubkey=None, format="PEM",
-                 cansign=False, password=None, callback=_cb):
+                 cansign=False, password=None):
+        """
+        PKey object can be created from either private/public key blob or
+        from C language pointer, returned by some OpenSSL function
+
+        Following named arguments are recognized by constructor
+
+        privkey - private key blob. If this is specified, format and
+             password can be also specified
+
+        pubkey - public key blob. If this is specified, format can be
+                specified.
+
+        ptr - pointer, returned by openssl function. If it is specified,    
+            cansign should be also specified.
+
+        These three arguments are mutually exclusive.
+
+        format - can be either 'PEM' or 'DER'. Specifies format of blob.
+        
+        password - can be string with password for encrypted key, or
+            callable  with one boolean argument, which returns password.
+            During constructor call this argument would be false.
+
+        If key is in PEM format, its encrypted status and format is
+        autodetected. If key is in DER format, than if password is
+        specified, key is assumed to be encrypted PKCS8 key otherwise
+        it is assumed to be unencrypted.
+        """
+
         if not ptr is None:
             self.key = ptr
             self.cansign = cansign
@@ -63,10 +104,15 @@ class PKey(object):
             self.cansign = True
             if format == "PEM":
                 self.key = libcrypto.PEM_read_bio_PrivateKey(bio.bio, None,
-                                                             callback,
-                                                             c_char_p(password))
+                                              _password_callback(password),
+                                               None)
             else:
-                self.key = libcrypto.d2i_PrivateKey_bio(bio.bio, None)
+                if password is not None:
+                    self.key = libcrypto.d2i_PKCS8PrivateKey_bio(bio.bio,None,
+                                           _password_callback(password),
+                                           None)
+                else:
+                    self.key = libcrypto.d2i_PrivateKey_bio(bio.bio, None)
             if self.key is None:
                 raise PKeyError("error parsing private key")
         elif not pubkey is None:
@@ -74,8 +120,8 @@ class PKey(object):
             self.cansign = False
             if format == "PEM":
                 self.key = libcrypto.PEM_read_bio_PUBKEY(bio.bio, None,
-                                                         callback,
-                                                         c_char_p(password))
+                                                         _password_callback(password),
+                                                         None)
             else:
                 self.key = libcrypto.d2i_PUBKEY_bio(bio.bio, None)
             if self.key is None:
@@ -249,13 +295,17 @@ class PKey(object):
             raise PKeyError("error serializing public key")
         return str(bio)
 
-    def exportpriv(self, format="PEM", password=None, cipher=None,
-                   callback=_cb):
+    def exportpriv(self, format="PEM", password=None, cipher=None):
         """
         Returns private key as PEM or DER Structure.
         If password and cipher are specified, encrypts key
         on given password, using given algorithm. Cipher must be
         an ctypescrypto.cipher.CipherType object
+
+        Password can be either string or function with one argument,
+        which returns password. It is called with argument True, which
+        means, that we are encrypting key, and password should be
+        verified (requested twice from user, for example).
         """
         bio = Membio()
         if cipher is None:
@@ -264,14 +314,14 @@ class PKey(object):
             evp_cipher = cipher.cipher
         if format == "PEM":
             ret = libcrypto.PEM_write_bio_PrivateKey(bio.bio, self.key,
-                                                     evp_cipher, None, 0,
-                                                     callback,
-                                                     c_char_p(password))
+                                         evp_cipher, None, 0,
+                                         _password_callback(password),
+                                         None)
         else:
             ret = libcrypto.i2d_PKCS8PrivateKey_bio(bio.bio, self.key,
-                                                    evp_cipher, None, 0,
-                                                    callback,
-                                                    c_char_p(password))
+                                               evp_cipher, None, 0,
+                                              _password_callback(password),
+                                               None)
         if ret == 0:
             raise PKeyError("error serializing private key")
         return str(bio)
@@ -355,4 +405,7 @@ libcrypto.i2d_PUBKEY_bio.argtypes = (c_void_p, c_void_p)
 libcrypto.i2d_PKCS8PrivateKey_bio.argtypes = (c_void_p, c_void_p, c_void_p,
                                               c_char_p, c_int,
                                               PW_CALLBACK_FUNC, c_char_p)
+libcrypto.d2i_PKCS8PrivateKey_bio.restype = c_void_p                                              
+libcrypto.d2i_PKCS8PrivateKey_bio.argtypes = (c_void_p,c_void_p,
+                                              PW_CALLBACK_FUNC,c_void_p)                                            
 libcrypto.ENGINE_finish.argtypes = (c_void_p, )
index 7e71e8c0cf88ab91bdabb22aac434f3bb89fa798..1018dc69a4caf94219de3fb09ed2a59f274f977f 100644 (file)
@@ -1,6 +1,7 @@
 from ctypescrypto.pkey import PKey
 import unittest
 from base64 import b64decode, b16decode
+from subprocess import Popen,PIPE,CalledProcessError
 
 def pem2der(s):
     start=s.find('-----\n')
@@ -8,6 +9,14 @@ def pem2der(s):
     data=s[start+6:finish]
     return b64decode(data)
 
+def runopenssl(args,indata):
+    p=Popen(['openssl']+args,stdin=PIPE,stdout=PIPE,stderr=PIPE,universal_newlines=True)
+    (out,err)=p.communicate(indata)
+    if p.returncode:
+        raise CalledProcessError(p.returncode," ".join(['openssl']+args)+":"+err)
+    return out
+
+
 class TestPKey(unittest.TestCase):
     rsa="""-----BEGIN PRIVATE KEY-----
 MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGBAL9CzVZu9bczTmB8
@@ -26,6 +35,44 @@ Ao6uTm8fnkD4C836wS4mYAPqwRBK1JvnEXEQee9irf+ip89BAg74ViTcGF9lwJwQ
 gOM+X5Db+3pK
 -----END PRIVATE KEY-----
 """
+    rsaenc="""-----BEGIN RSA PRIVATE KEY-----
+Proc-Type: 4,ENCRYPTED
+DEK-Info: AES-256-CBC,7FF0E46291D60D35ACA881131C244655
+
+BeJoui1lRQDvvPr+gH8xCdqkcgKCLWpZTvFZmvrqXmPqMHpm20nK0ESAd6kKm8d1
+zaglRIHnnO6V7aDcwgOd3IYPEOnG2TIRQniZrwZdrXIfacscJ6Ekq+5YfLuyrRgq
+fscGl7ntm/eGLqwrhzuv7jAXpn9QWiiAld0EWcmZCAW7nGaUQtu4rc4ULwL5SC/M
+MOCPwpcD3SCQv55dX3cBOtfZ3lPbpgEpTpnNnj8OtxOkkIaG8yol7luwHvoOSyL/
+WuXGCpfJE4LzbxnSLhbiN7q+y/Sro3cGc9aO4tXToMqTFel4zqR0YgOeFazlDRi1
+mPuZcGLuSIef0kJn7Mg7jt0DQk579rTVxAhIu2rylTwEozkpCp5g4kGTJON++HQr
+BRrApm4XlAoH2GX1jqDBoWSnXCRH49jNGQrLwy469i+994cG8uVU9Z5cqm/LDIR9
+kwQfTJIvMi0g28NBMVgJ2gLj40OczxDGyNvBIbhPNswHljfsvPVr4vtxDGx8fS0N
+lUJUOL9me+XNZ5xGHYuT5DOr7GE+H3hKEg+XfrYEete9BeI4gm9cqESvrLY9EU5Q
+tOtnKKL7SglTZ5LxPMAedADC0o01nzr+D3gAiOhSgoZTrnQsSZ7iTJOtm3vNXwJx
+AgesYmXtr5mdiBPKQ1QA/jF5LUZji+5KENd5WHNQw7tOlMLDrPFVRfLZg1AQDljx
+u16kdyb71Kk3f6GCOfUntGr+kzppc3DDT+RcLetXphOOEQRy6C6/wmz08WlAPlu5
+mFfSDijpWxoUHooQISg5mE82oR8V81aBpbLtm7KevwY=
+-----END RSA PRIVATE KEY-----
+"""
+    pkcs8crypt="""-----BEGIN ENCRYPTED PRIVATE KEY-----
+MIICoTAbBgkqhkiG9w0BBQMwDgQIipVEnsV/gQoCAggABIICgE1i42C4aBhykhOi
+EItFRE+9iBgiklGxoCJtukdp1UwDRKy/GJJ1rcS385CQy4Rs0zN8NH1faVRbf4Vt
+iNACHtJx30qMCdo64CR+GJYHS4g2lGaz7PFNma8SjnAbGYXwXkdm5zhwmiU++wC7
+W59u8oWS8Dj9dZBMzoOQGQT6xzZwQ14H65zHvC16HdKSNtRgXDWkBnD2cQzuOyuf
+rFLyEf7/FH6B7/yKDcwsEfu97uPPxMvuusD1UubWnltO/Hc2oCPibN+dGw1PY9mC
+18yGQtZkf5z30yhLosF62IVy3XY9Yf/TJYojIExoASrThGRvENzWkQ3qfnErqmng
+l+dy66bmLjicobF5bO3xAhpU1dL+4/1ba2QuthVNlg6Or/iII1ntNN4PFyYcEwmX
+e09C3dyOtV7qCq13S1bRkbZhzwi2QbLKALAzrZpF6VYmayTz8KjQOZ8BncAM+BiI
+CtwuZJoXLW9kT4D7UsaSZdjUvzBIak5qdCGWpKmahMfjEEsCg6ApuIYmFrCgiY9c
+0keYjY8DJ+4bEvqsQvTIaU9F9mFytI1E3LnR0NP1jHuOA7Jc+oNQ2adgFNj12jKQ
+qNt1bEGNCqQHSrw7JNCrB7s+QAFNqJtno6fIq7vVNkqadJlnBbCIgm7NlJeGg9j6
+a5YVNGlbs0J4dQF4Jw13302IBn3piSzthWL2gL98v/1lEwGuernEpPAjry3YhzM9
+VA/oVt22n3yVA6dOSVL1oUTJyawEqASmH0jHAzXNDz+QLSLmz82ARcZPqPvVc45e
+5h0xtqtFVkQLNbYzpNWGrx7R1hdr84nOKa8EsIxTRgEL/w9Y4Z/3xEoK2+KVBpMk
+oxUuxuU=
+-----END ENCRYPTED PRIVATE KEY-----
+"""
+    password="1111"
     rsakeytext="""Public-Key: (1024 bit)
 Modulus:
     00:bf:42:cd:56:6e:f5:b7:33:4e:60:7c:ef:be:a9:
@@ -62,12 +109,48 @@ AvhqdgCWLMG0D4Rj4oCqJcyG2WH8J5+0DnGujfEA4TwJ90ECvLa2SA==
     
     def test_unencrypted_pem(self):
         key=PKey(privkey=self.rsa)
+        self.assertTrue(key.cansign)
+        self.assertIsNotNone(key.key)
+        self.assertEqual(str(key),self.rsakeytext)
+    def test_encrypted_pem(self):
+        key=PKey(privkey=self.rsaenc,password=self.password)
+        self.assertIsNotNone(key.key)
+        self.assertEqual(str(key),self.rsakeytext)
+    def test_encrypted_pem_cb(self):
+        cb=lambda x:self.password
+        key=PKey(privkey=self.rsaenc,password=cb)
+        self.assertIsNotNone(key.key)
+        self.assertEqual(str(key),self.rsakeytext)
+    def test_encryped_pem_pkcs8(self):
+        key=PKey(privkey=self.pkcs8crypt,password=self.password)
+        self.assertIsNotNone(key.key)
+        self.assertEqual(str(key),self.rsakeytext)
+    def test_encrypted_der_pkcs8(self):
+        pkcs8der = pem2der(self.pkcs8crypt)
+        key=PKey(privkey=pkcs8der,password=self.password,format="DER")
         self.assertIsNotNone(key.key)
         self.assertEqual(str(key),self.rsakeytext)
     def test_export_priv_pem(self):
         key=PKey(privkey=self.ec1priv)
         out=key.exportpriv()
         self.assertEqual(self.ec1priv,out)
+    def test_export_priv_encrypt(self):
+        from ctypescrypto.cipher import CipherType
+        key=PKey(privkey=self.rsa)
+        pem=key.exportpriv(password='2222',cipher=CipherType("aes256"))
+        self.assertEqual(runopenssl(["pkey","-text_pub","-noout","-passin","pass:2222"],
+                                    pem),self.rsakeytext)
+    def test_export_priv_der(self):
+        key=PKey(privkey=self.rsa)
+        der=key.exportpriv(format="DER")
+        self.assertEqual(runopenssl(["pkey","-text_pub","-noout","-inform","DER"],
+            der),self.rsakeytext)
+    def test_export_priv_der_enc(self):
+        from ctypescrypto.cipher import CipherType
+        key=PKey(privkey=self.rsa)
+        der=key.exportpriv(format="DER",password='2222',cipher=CipherType("aes256"))
+        self.assertEqual(runopenssl(["pkcs8","-passin","pass:2222","-inform","DER"],
+            der),self.rsa)
     def test_unencrypted_pem_ec(self):
         
         key=PKey(privkey=self.ec1priv)