Repository: privacyidea/privacyidea

Issue:
Enrollment of certificate token fails under Python 3
Steps to reproduce:
* Run privacyIDEA under Python 3
* Create a local CA + connector, e.g. with ``pi-manage ca create``
* Enroll a certificate token:

```shell
$ http POST 'http://localhost:5000/token/init' PI-Authorization:$TOKEN type=certificate genkey=1 ca=myCA user=user123                                                                    1 ↵
HTTP/1.0 500 INTERNAL SERVER ERROR
Cache-Control: no-cache
Content-Length: 765
Content-Type: application/json
Date: Mon, 19 Aug 2019 14:06:52 GMT
Server: Werkzeug/0.14.1 Python/3.7.4

{
    "detail": null,
    "id": 1,
    "jsonrpc": "2.0",
    "result": {
        "error": {
            "code": -500,
            "message": "Object of type bytes is not JSON serializable"
        },
        "status": false
    },
    "signature": "...",
    "time": 1566223612.875488,
    "version": "privacyIDEA 3.0.2.dev2"
}
```

Traceback:
```
[2019-08-19 16:06:52,849] ERROR in app: Exception on /token/init [POST]
Traceback (most recent call last):
  File "/home/fred/privacyidea/privacyidea/venv3/lib/python3.7/site-packages/flask/app.py", line 2292, in wsgi_app
    response = self.full_dispatch_request()
  File "/home/fred/privacyidea/privacyidea/venv3/lib/python3.7/site-packages/flask/app.py", line 1815, in full_dispatch_request
    rv = self.handle_user_exception(e)
  File "/home/fred/privacyidea/privacyidea/venv3/lib/python3.7/site-packages/flask/app.py", line 1718, in handle_user_exception
    reraise(exc_type, exc_value, tb)
  File "/home/fred/privacyidea/privacyidea/venv3/lib/python3.7/site-packages/flask/_compat.py", line 35, in reraise
    raise value
  File "/home/fred/privacyidea/privacyidea/venv3/lib/python3.7/site-packages/flask/app.py", line 1813, in full_dispatch_request
    rv = self.dispatch_request()
  File "/home/fred/privacyidea/privacyidea/venv3/lib/python3.7/site-packages/flask/app.py", line 1799, in dispatch_request
    return self.view_functions[rule.endpoint](**req.view_args)
  File "/home/fred/privacyidea/privacyidea/privacyidea/api/lib/prepolicy.py", line 129, in policy_wrapper
    return wrapped_function(*args, **kwds)
  File "/home/fred/privacyidea/privacyidea/privacyidea/api/lib/prepolicy.py", line 129, in policy_wrapper
    return wrapped_function(*args, **kwds)
  File "/home/fred/privacyidea/privacyidea/privacyidea/api/lib/prepolicy.py", line 129, in policy_wrapper
    return wrapped_function(*args, **kwds)
  [Previous line repeated 15 more times]
  File "/home/fred/privacyidea/privacyidea/privacyidea/api/lib/postpolicy.py", line 102, in policy_wrapper
    response = wrapped_function(*args, **kwds)
  File "/home/fred/privacyidea/privacyidea/privacyidea/lib/subscriptions.py", line 333, in check_subscription_wrapper
    f_result = func(*args, **kwds)
  File "/home/fred/privacyidea/privacyidea/privacyidea/lib/event.py", line 99, in event_wrapper
    f_result = func(*args, **kwds)
  File "/home/fred/privacyidea/privacyidea/privacyidea/lib/log.py", line 154, in log_wrapper
    return func(*args, **kwds)
  File "/home/fred/privacyidea/privacyidea/privacyidea/api/token.py", line 291, in init
    return send_result(True, details=response_details)
  File "/home/fred/privacyidea/privacyidea/privacyidea/api/lib/utils.py", line 104, in send_result
    return jsonify(prepare_result(obj, rid, details))
  File "/home/fred/privacyidea/privacyidea/venv3/lib/python3.7/site-packages/flask/json/__init__.py", line 321, in jsonify
    dumps(data, indent=indent, separators=separators) + '\n',
  File "/home/fred/privacyidea/privacyidea/venv3/lib/python3.7/site-packages/flask/json/__init__.py", line 179, in dumps
    rv = _json.dumps(obj, **kwargs)
  File "/usr/lib64/python3.7/json/__init__.py", line 238, in dumps
    **kw).encode(obj)
  File "/usr/lib64/python3.7/json/encoder.py", line 199, in encode
    chunks = self.iterencode(o, _one_shot=True)
  File "/usr/lib64/python3.7/json/encoder.py", line 257, in iterencode
    return _iterencode(o, 0)
  File "/home/fred/privacyidea/privacyidea/venv3/lib/python3.7/site-packages/flask/json/__init__.py", line 81, in default
    return _json.JSONEncoder.default(self, o)
  File "/usr/lib64/python3.7/json/encoder.py", line 179, in default
    raise TypeError(f'Object of type {o.__class__.__name__} '
TypeError: Object of type bytes is not JSON serializable
```


Discussion:
This is due to ``CertificateToken.get_as_dict()/get_init_detail()`` writing adding PKCS12 data (as a bytestring) to the enrollment response:
https://github.com/privacyidea/privacyidea/blob/7f23b1ea7961f14d3b64444d0b88afa77fa6e4bf/privacyidea/lib/tokens/certificatetoken.py#L288

Resolving patch:
diff --git a/privacyidea/lib/tokens/certificatetoken.py b/privacyidea/lib/tokens/certificatetoken.py
--- a/privacyidea/lib/tokens/certificatetoken.py
+++ b/privacyidea/lib/tokens/certificatetoken.py
@@ -33,7 +33,7 @@
 
 import logging
 
-from privacyidea.lib.utils import to_unicode
+from privacyidea.lib.utils import to_unicode, b64encode_and_unicode
 from privacyidea.lib.tokenclass import TokenClass
 from privacyidea.lib.log import log_with
 from privacyidea.api.lib.utils import getParam
@@ -41,7 +41,6 @@
 from privacyidea.lib.user import get_user_from_param
 from OpenSSL import crypto
 from privacyidea.lib.decorators import check_token_locked
-import base64
 from privacyidea.lib import _
 
 optional = True
@@ -246,8 +245,7 @@ def get_init_detail(self, params=None, user=None):
         privatekey = self.get_tokeninfo("privatekey")
         # If there is a private key, we dump a PKCS12
         if privatekey:
-            response_detail["pkcs12"] = base64.b64encode(
-                self._create_pkcs12_bin())
+            response_detail["pkcs12"] = b64encode_and_unicode(self._create_pkcs12_bin())
 
         return response_detail
 
@@ -285,9 +283,7 @@ def get_as_dict(self):
         token_dict = self.token.get()
 
         if "privatekey" in token_dict.get("info"):
-            token_dict["info"]["pkcs12"] = base64.b64encode(
-                self._create_pkcs12_bin())
-            #del(token_dict["privatekey"])
+            token_dict["info"]["pkcs12"] = b64encode_and_unicode(self._create_pkcs12_bin())
 
         return token_dict
 


Test patch:
diff --git a/tests/test_api_token.py b/tests/test_api_token.py
--- a/tests/test_api_token.py
+++ b/tests/test_api_token.py
@@ -1184,6 +1184,7 @@ def test_17_enroll_certificate(self):
                               "CertificateDir": "",
                               "WorkingDir": cwd + "/" + WORKINGDIR})
 
+        # Enroll a certificate token with a CSR
         with self.app.test_request_context('/token/init',
                                            data={"type": "certificate",
                                                  "request": REQUEST,
@@ -1198,6 +1199,31 @@ def test_17_enroll_certificate(self):
             certificate = detail.get("certificate")
             self.assertTrue("-----BEGIN CERTIFICATE-----" in certificate)
 
+        # Enroll a certificate token, also generating a private key
+        with self.app.test_request_context('/token/init',
+                                           data={"type": "certificate",
+                                                 "genkey": "1",
+                                                 "user": "cornelius",
+                                                 "realm": self.realm1,
+                                                 "ca": "localCA"},
+                                           method="POST",
+                                           headers={'Authorization': self.at}):
+            res = self.app.full_dispatch_request()
+            self.assertTrue(res.status_code == 200, res)
+            result = json.loads(res.data.decode('utf8')).get("result")
+            self.assertTrue(result.get("value"))
+            detail = json.loads(res.data.decode('utf8')).get("detail")
+            self.assertIn("pkcs12", detail)
+
+        # List tokens
+        with self.app.test_request_context('/token/?type=certificate',
+                                           method="GET",
+                                           headers={'Authorization': self.at}):
+            res = self.app.full_dispatch_request()
+            self.assertTrue(res.status_code == 200, res)
+            result = json.loads(res.data.decode('utf8')).get("result")
+            self.assertEqual(len(result["value"]["tokens"]), 2)
+
     def test_18_revoke_token(self):
         self._create_temp_token("RevToken")