Repository: randombit/botan
Pull request #2165 (2019-10-18 20:16:25)

Add missing x509 python functions
I implemented the missing funcionality regarding X509 certificates mentioned in #1906. I am open to all criticism and requests for changes.

randombit: Awesome thank you. I'll review this soon. As initial comment can you add some tests (in `src/scripts/test_python.py`)?
zacikpa: Sure, will do so.
codecov-io: # [Codecov](https://codecov.io/gh/randombit/botan/pull/2165?src=pr&el=h1) Report
> Merging [#2165](https://codecov.io/gh/randombit/botan/pull/2165?src=pr&el=desc) into [master](https://codecov.io/gh/randombit/botan/commit/bbef28707d07cf0729911144be7d18a3061b2fef?src=pr&el=desc) will **decrease** coverage by `<.01%`.
> The diff coverage is `n/a`.

[![Impacted file tree graph](https://codecov.io/gh/randombit/botan/pull/2165/graphs/tree.svg?width=650&token=VR6iYmSPH8&height=150&src=pr)](https://codecov.io/gh/randombit/botan/pull/2165?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master    #2165      +/-   ##
==========================================
- Coverage   92.35%   92.34%   -0.01%     
==========================================
  Files         553      553              
  Lines       60186    60186              
  Branches     6375     6375              
==========================================
- Hits        55582    55581       -1     
- Misses       4604     4605       +1
```


| [Impacted Files](https://codecov.io/gh/randombit/botan/pull/2165?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [src/lib/asn1/der\_enc.cpp](https://codecov.io/gh/randombit/botan/pull/2165/diff?src=pr&el=tree#diff-c3JjL2xpYi9hc24xL2Rlcl9lbmMuY3Bw) | `83.85% <0%> (-2.49%)` | :arrow_down: |
| [src/lib/pbkdf/bcrypt\_pbkdf/bcrypt\_pbkdf.cpp](https://codecov.io/gh/randombit/botan/pull/2165/diff?src=pr&el=tree#diff-c3JjL2xpYi9wYmtkZi9iY3J5cHRfcGJrZGYvYmNyeXB0X3Bia2RmLmNwcA==) | `91.66% <0%> (ø)` | :arrow_up: |
| [src/lib/pubkey/mce/mceliece\_key.cpp](https://codecov.io/gh/randombit/botan/pull/2165/diff?src=pr&el=tree#diff-c3JjL2xpYi9wdWJrZXkvbWNlL21jZWxpZWNlX2tleS5jcHA=) | `86.51% <0%> (+1.12%)` | :arrow_up: |
| [src/lib/pk\_pad/emsa\_raw/emsa\_raw.cpp](https://codecov.io/gh/randombit/botan/pull/2165/diff?src=pr&el=tree#diff-c3JjL2xpYi9wa19wYWQvZW1zYV9yYXcvZW1zYV9yYXcuY3Bw) | `74.28% <0%> (+2.85%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/randombit/botan/pull/2165?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/randombit/botan/pull/2165?src=pr&el=footer). Last update [bbef287...703c52f](https://codecov.io/gh/randombit/botan/pull/2165?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

zacikpa: Could have opened this as draft PR instead. Anyway, new commits are coming soon.

Patch:
diff --git a/doc/api_ref/python.rst b/doc/api_ref/python.rst
index 35a77684c..80c1ca89d 100644
--- a/doc/api_ref/python.rst
+++ b/doc/api_ref/python.rst
@@ -532,3 +532,124 @@ HOTP
       counter value is always identical to the counter that was passed
       in. If the code did verify and resync_range was zero, then the
       next counter will always be counter+1.
+
+X509Cert
+-----------------------------------------
+
+.. py:class:: X509Cert(filename=None, buf=None)
+
+   .. py:method:: time_starts()
+
+      Return the time the certificate becomes valid, as a string in form
+      "YYYYMMDDHHMMSSZ" where Z is a literal character reflecting that this time is
+      relative to UTC.
+
+   .. py:method:: time_expires()
+
+      Return the time the certificate expires, as a string in form
+      "YYYYMMDDHHMMSSZ" where Z is a literal character reflecting that this time is
+      relative to UTC.      
+
+   .. py:method:: to_string()
+
+      Format the certificate as a free-form string.
+
+   .. py:method:: fingerprint(hash_algo='SHA-256')
+
+      Return a fingerprint for the certificate, which is basically just a hash
+      of the binary contents. Normally SHA-1 or SHA-256 is used, but any hash
+      function is allowed.
+
+   .. py:method:: serial_number()
+
+      Return the serial number of the certificate.
+
+   .. py:method:: authority_key_id()
+
+      Return the authority key ID set in the certificate, which may be empty.
+
+   .. py:method:: subject_key_id()
+
+      Return the subject key ID set in the certificate, which may be empty.
+
+   .. py:method:: subject_public_key_bits()
+
+      Get the serialized representation of the public key included in this certificate.
+
+   .. py:method:: subject_public_key()
+
+      Get the public key included in this certificate as an object of class ``PublicKey``.
+
+   .. py:method:: subject_dn(key, index)
+
+      Get a value from the subject DN field.
+
+      ``key`` specifies a value to get, for instance ``"Name"`` or `"Country"`.  
+
+   .. py:method:: issuer_dn(key, index)
+
+      Get a value from the issuer DN field.
+
+      ``key`` specifies a value to get, for instance ``"Name"`` or `"Country"`.
+
+   .. py:method:: hostname_match(hostname)
+
+      Return True if the Common Name (CN) field of the certificate matches a given ``hostname``.
+
+   .. py:method:: not_before()
+
+      Return the time the certificate becomes valid, as seconds since epoch.   
+
+   .. py:method:: not_after()
+
+      Return the time the certificate expires, as seconds since epoch.
+
+   .. py:method:: allowed_usage(usage_list)
+
+      Return True if the certificates Key Usage extension contains all constraints given in ``usage_list``.
+      Also return True if the certificate doesn't have this extension.
+      Example usage constraints are: ``"DIGITAL_SIGNATURE"``, ``"KEY_CERT_SIGN"``, ``"CRL_SIGN"``.
+
+   .. py:method:: verify(intermediates=None, \
+                  trusted=None, \
+                  trusted_path=None, \
+                  required_strength=0, \
+                  hostname=None, \
+                  reference_time=0)
+
+      Verify a certificate. Returns 0 if validation was successful, returns a positive error code 
+      if the validation was unsuccesful.
+
+      ``intermediates`` is a list of untrusted subauthorities.
+
+      ``trusted`` is a list of trusted root CAs.
+
+      The `trusted_path` refers to a directory where one or more trusted CA
+      certificates are stored.
+
+      Set ``required_strength`` to indicate the minimum key and hash strength
+      that is allowed. For instance setting to 80 allows 1024-bit RSA and SHA-1.
+      Setting to 110 requires 2048-bit RSA and SHA-256 or higher. Set to zero
+      to accept a default.
+
+      If ``hostname`` is given, it will be checked against the certificates CN field.
+
+      Set ``reference_time`` to be the time which the certificate chain is
+      validated against. Use zero (default) to use the current system clock.
+
+   .. py:classmethod:: validation_status(error_code)
+
+      Return an informative string associated with the verification return code.
+
+     
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/python/botan2.py b/src/python/botan2.py
index 7db8cd9b3..2018cae20 100755
--- a/src/python/botan2.py
+++ b/src/python/botan2.py
@@ -376,7 +376,7 @@ def _set_prototypes(dll):
             [c_void_p, c_char_p, c_size_t, c_char_p, POINTER(c_size_t)])
     ffi_api(dll.botan_x509_cert_to_string, [c_void_p, c_char_p, POINTER(c_size_t)])
     ffi_api(dll.botan_x509_cert_allowed_usage, [c_void_p, c_uint])
-    ffi_api(dll.botan_x509_cert_hostname_match, [c_void_p, c_char_p])
+    ffi_api(dll.botan_x509_cert_hostname_match, [c_void_p, c_char_p], [-1])
     ffi_api(dll.botan_x509_cert_verify,
             [POINTER(c_int), c_void_p, c_void_p, c_size_t, c_void_p, c_size_t, c_char_p, c_size_t, c_char_p, c_uint64])
 
@@ -1338,6 +1338,82 @@ class X509Cert(object): # pylint: disable=invalid-name
         return _call_fn_returning_str(
             0, lambda b, bl: _DLL.botan_x509_cert_get_subject_dn(self.__obj, _ctype_str(key), index, b, bl))
 
+    def issuer_dn(self, key, index):
+        return _call_fn_returning_str(
+            0, lambda b, bl: _DLL.botan_x509_cert_get_issuer_dn(self.__obj, _ctype_str(key), index, b, bl))
+
+    def hostname_match(self, hostname):
+        rc = _DLL.botan_x509_cert_hostname_match(self.__obj, _ctype_str(hostname))
+        return rc == 0
+
+    def not_before(self):
+        time = c_uint64(0)
+        _DLL.botan_x509_cert_not_before(self.__obj, byref(time))
+        return time.value
+
+    def not_after(self):
+        time = c_uint64(0)
+        _DLL.botan_x509_cert_not_after(self.__obj, byref(time))
+        return time.value
+
+    def allowed_usage(self, usage_list):
+        usage_values = {"NO_CONSTRAINTS": 0,
+                        "DIGITAL_SIGNATURE": 32768,
+                        "NON_REPUDIATION": 16384,
+                        "KEY_ENCIPHERMENT": 8192,
+                        "DATA_ENCIPHERMENT": 4096,
+                        "KEY_AGREEMENT": 2048,
+                        "KEY_CERT_SIGN": 1024,
+                        "CRL_SIGN": 512,
+                        "ENCIPHER_ONLY": 256,
+                        "DECIPHER_ONLY": 128}
+        usage = 0
+        for u in usage_list:
+            if u not in usage_values:
+                return False
+            usage += usage_values[u]
+
+        rc = _DLL.botan_x509_cert_allowed_usage(self.__obj, c_uint(usage))
+        return rc == 0
+
+    def get_obj(self):
+        return self.__obj
+
+    def verify(self,
+               intermediates=None,
+               trusted=None,
+               trusted_path=None,
+               required_strength=0,
+               hostname=None,
+               reference_time=0):
+
+        c_intermediates = len(intermediates) * c_void_p
+        arr_intermediates = c_intermediates()
+        for i, ca in enumerate(intermediates):
+            arr_intermediates[i] = ca.get_obj()
+
+        c_trusted = len(trusted) * c_void_p
+        arr_trusted = c_trusted()
+        for i, ca in enumerate(trusted):
+            arr_trusted[i] = ca.get_obj()
+
+        error_code = c_int(0)
+
+        _DLL.botan_x509_cert_verify(byref(error_code),
+                                    self.__obj,
+                                    byref(arr_intermediates),
+                                    c_size_t(len(intermediates)),
+                                    byref(arr_trusted),
+                                    c_size_t(len(trusted)),
+                                    _ctype_str(trusted_path),
+                                    c_size_t(required_strength),
+                                    _ctype_str(hostname),
+                                    c_uint64(reference_time))
+        return error_code.value
+
+    @classmethod
+    def validation_status(cls, error_code):
+        return _ctype_to_str(_DLL.botan_x509_cert_validation_status(c_int(error_code)))
 
 class MPI(object):
 


Test patch:
diff --git a/src/scripts/test_python.py b/src/scripts/test_python.py
index e860df358..60cb80ec5 100644
--- a/src/scripts/test_python.py
+++ b/src/scripts/test_python.py
@@ -463,6 +463,41 @@ ofvkP1EDmpx50fHLawIDAQAB
 
         self.assertTrue(cert.to_string().startswith("Version: 3"))
 
+        self.assertEqual(cert.issuer_dn('Name', 0), 'csca-germany')
+        self.assertEqual(cert.issuer_dn('Organization', 0), 'bund')
+        self.assertEqual(cert.issuer_dn('Organizational Unit', 0), 'bsi')
+        self.assertEqual(cert.issuer_dn('Country', 0), 'DE')
+
+        self.assertTrue(cert.hostname_match('csca-germany'))
+        self.assertFalse(cert.hostname_match('csca-slovakia'))
+
+        self.assertEqual(cert.not_before(), 1184858838)
+        self.assertEqual(cert.not_after(), 1831907880)
+
+        self.assertTrue(cert.allowed_usage(["CRL_SIGN", "KEY_CERT_SIGN"]))
+        self.assertTrue(cert.allowed_usage(["KEY_CERT_SIGN"]))
+        self.assertFalse(cert.allowed_usage(["DIGITAL_SIGNATURE"]))
+        self.assertFalse(cert.allowed_usage(["DIGITAL_SIGNATURE", "CRL_SIGN"]))
+
+        root = botan2.X509Cert("src/tests/data/x509/nist/root.crt")
+
+        int09 = botan2.X509Cert("src/tests/data/x509/nist/test09/int.crt")
+        end09 = botan2.X509Cert("src/tests/data/x509/nist/test09/end.crt")
+        self.assertEqual(end09.verify([int09], [root]), 2001)
+
+        end04 = botan2.X509Cert("src/tests/data/x509/nist/test04/end.crt")
+        int04_1 = botan2.X509Cert("src/tests/data/x509/nist/test04/int1.crt")
+        int04_2 = botan2.X509Cert("src/tests/data/x509/nist/test04/int2.crt")
+        self.assertEqual(end04.verify([int04_1, int04_2], [], "src/tests/data/x509/nist/", required_strength=80), 0)
+        self.assertEqual(end04.verify([int04_1, int04_2], [], required_strength=80), 3000)
+        self.assertEqual(end04.verify([int04_1, int04_2], [root], required_strength=80, hostname="User1-CP.02.01"), 0)
+        self.assertEqual(end04.verify([int04_1, int04_2], [root], required_strength=80, hostname="invalid"), 4008)
+        self.assertEqual(end04.verify([int04_1, int04_2], [root], required_strength=80, reference_time=1), 2000)
+
+        self.assertEqual(botan2.X509Cert.validation_status(0), 'Verified')
+        self.assertEqual(botan2.X509Cert.validation_status(3000), 'Certificate issuer not found')
+        self.assertEqual(botan2.X509Cert.validation_status(4008), 'Certificate does not match provided name')
+
     def test_mpi(self):
         # pylint: disable=too-many-statements
         z = botan2.MPI()