[pyOpenSSL] [PATCH] add support for handling trusted certificates

Joe Orton jorton at redhat.com
Tue Apr 20 17:42:40 CEST 2010


Hi.  I've created a branch here:

https://code.launchpad.net/~jorton/pyopenssl/trust

which adds basic support for handling of trusted certificates in 
pyOpenSSL.  The patch is attached for review.

Regards, Joe
-------------- next part --------------
# Bazaar merge directive format 2 (Bazaar 0.90)
# revision_id: jorton at apache.org-20100420153137-mjtfi93na6lqn3k6
# target_branch: bzr+ssh://bazaar.launchpad.net/~exarkun/pyopenssl\
#   /trunk/
# testament_sha1: 03ea7c76f06f3c09235b1fa4ef7c62376f8237b6
# timestamp: 2010-04-20 16:33:44 +0100
# base_revision_id: exarkun at divmod.com-20100211142224-gsb68klzpls15mni
# 
# Begin patch
=== added file '.bzrignore'
--- .bzrignore	1970-01-01 00:00:00 +0000
+++ .bzrignore	2010-04-20 12:07:32 +0000
@@ -0,0 +1,2 @@
+build
+_trial_temp

=== modified file 'src/crypto/crypto.c'
--- src/crypto/crypto.c	2009-07-17 17:50:12 +0000
+++ src/crypto/crypto.c	2010-04-20 15:31:37 +0000
@@ -328,6 +328,113 @@
     return buffer;
 }
 
+static char crypto_load_trusted_certificate_doc[] = "\n\
+Load a trusted certificate from a buffer\n\
+\n\
+ at param type: The file type (one of FILETYPE_PEM, FILETYPE_ASN1)\n\
+             buffer - The buffer the certificate is stored in\n\
+ at return: The X509 object\n\
+";
+
+static PyObject *
+crypto_load_trusted_certificate(PyObject *spam, PyObject *args)
+{
+    crypto_X509Obj *crypto_X509_New(X509 *, int);
+    int type, len;
+    char *buffer;
+    BIO *bio;
+    X509 *cert;
+
+    if (!PyArg_ParseTuple(args, "is#:load_trusted_certificate", &type, &buffer, &len))
+        return NULL;
+
+    bio = BIO_new_mem_buf(buffer, len);
+    switch (type)
+    {
+        case X509_FILETYPE_PEM:
+            cert = PEM_read_bio_X509_AUX(bio, NULL, NULL, NULL);
+            break;
+
+#if 0 /* not implemented yet */
+        case X509_FILETYPE_ASN1:
+            cert = d2i_X509_AUX_bio(bio, NULL);
+            break;
+#endif
+
+        default:
+            PyErr_SetString(PyExc_ValueError, "type argument must be FILETYPE_PEM");
+            BIO_free(bio);
+            return NULL;
+    }
+    BIO_free(bio);
+
+    if (cert == NULL)
+    {
+        exception_from_error_queue(crypto_Error);
+        return NULL;
+    }
+
+    return (PyObject *)crypto_X509_New(cert, 1);
+}
+
+static char crypto_dump_trusted_certificate_doc[] = "\n\
+Dump a trusted certificate to a buffer\n\
+\n\
+ at param type: The file type (one of FILETYPE_PEM, FILETYPE_ASN1)\n\
+ at param cert: The certificate to dump\n\
+ at return: The buffer with the dumped certificate in\n\
+";
+
+static PyObject *
+crypto_dump_trusted_certificate(PyObject *spam, PyObject *args)
+{
+    int type, ret, buf_len;
+    char *temp;
+    PyObject *buffer;
+    BIO *bio;
+    crypto_X509Obj *cert;
+
+    if (!PyArg_ParseTuple(args, "iO!:dump_trusted_certificate", &type,
+			  &crypto_X509_Type, &cert))
+        return NULL;
+
+    bio = BIO_new(BIO_s_mem());
+    switch (type)
+    {
+        case X509_FILETYPE_PEM:
+            ret = PEM_write_bio_X509_AUX(bio, cert->x509);
+            break;
+
+#if 0 /* not implemented */
+        case X509_FILETYPE_ASN1:
+            ret = i2d_X509_bio(bio, cert->x509);
+            break;
+
+        case X509_FILETYPE_TEXT:
+            ret = X509_print_ex(bio, cert->x509, 0, 0);
+            break;
+#endif
+
+        default:
+            PyErr_SetString(PyExc_ValueError, "type argument must be FILETYPE_PEM");
+            BIO_free(bio);
+            return NULL;
+    }
+
+    if (ret == 0)
+    {
+        BIO_free(bio);
+        exception_from_error_queue(crypto_Error);
+        return NULL;
+    }
+
+    buf_len = BIO_get_mem_data(bio, &temp);
+    buffer = PyString_FromStringAndSize(temp, buf_len);
+    BIO_free(bio);
+
+    return buffer;
+}
+
 static char crypto_load_certificate_request_doc[] = "\n\
 Load a certificate request from a buffer\n\
 \n\
@@ -553,6 +660,9 @@
     { "dump_privatekey",  (PyCFunction)crypto_dump_privatekey,  METH_VARARGS, crypto_dump_privatekey_doc },
     { "load_certificate", (PyCFunction)crypto_load_certificate, METH_VARARGS, crypto_load_certificate_doc },
     { "dump_certificate", (PyCFunction)crypto_dump_certificate, METH_VARARGS, crypto_dump_certificate_doc },
+
+    { "load_trusted_certificate", (PyCFunction)crypto_load_trusted_certificate, METH_VARARGS, crypto_load_trusted_certificate_doc },
+    { "dump_trusted_certificate", (PyCFunction)crypto_dump_trusted_certificate, METH_VARARGS, crypto_dump_trusted_certificate_doc },
     { "load_certificate_request", (PyCFunction)crypto_load_certificate_request, METH_VARARGS, crypto_load_certificate_request_doc },
     { "dump_certificate_request", (PyCFunction)crypto_dump_certificate_request, METH_VARARGS, crypto_dump_certificate_request_doc },
     { "load_pkcs7_data", (PyCFunction)crypto_load_pkcs7_data, METH_VARARGS, crypto_load_pkcs7_data_doc },
@@ -667,6 +777,13 @@
     if (PyModule_AddObject(module, "Error", crypto_Error) != 0)
         goto error;
 
+    PyModule_AddStringConstant(module, "TRUST_SSL_SERVER", SN_server_auth);
+    PyModule_AddStringConstant(module, "TRUST_SSL_CLIENT", SN_client_auth);
+    PyModule_AddStringConstant(module, "TRUST_OBJECT_SIGN", SN_code_sign);
+    PyModule_AddStringConstant(module, "TRUST_EMAIL", SN_email_protect);
+    PyModule_AddStringConstant(module, "TRUST_TSA", SN_time_stamp);
+    PyModule_AddStringConstant(module, "TRUST_OCSP_SIGN", SN_OCSP_sign);
+
     PyModule_AddIntConstant(module, "FILETYPE_PEM",  X509_FILETYPE_PEM);
     PyModule_AddIntConstant(module, "FILETYPE_ASN1", X509_FILETYPE_ASN1);
     PyModule_AddIntConstant(module, "FILETYPE_TEXT", X509_FILETYPE_TEXT);

=== modified file 'src/crypto/x509.c'
--- src/crypto/x509.c	2009-09-01 14:35:50 +0000
+++ src/crypto/x509.c	2010-04-20 15:31:37 +0000
@@ -3,6 +3,7 @@
  *
  * Copyright (C) AB Strakt 2001, All rights reserved
  * Copyright (C) Jean-Paul Calderone 2008, All rights reserved
+ * Copyright (C) Red Hat, Inc. 2010.
  *
  * Certificate (X.509) handling code, mostly thin wrappers around OpenSSL.
  * See the file RATIONALE for a short explanation of why this module was written.
@@ -688,6 +689,170 @@
     return Py_None;
 }
 
+/* Return sorted list object with short names of objects in stack. */
+static PyObject *
+crypto_X509_stack_to_list(STACK_OF(ASN1_OBJECT *) stack)
+{
+    PyObject *list;
+    int i, n;
+
+    n = sk_ASN1_OBJECT_num(stack);
+    list = PyList_New(n);
+    
+    for (i = 0; i < n; i++) 
+    {
+        ASN1_OBJECT *obj = sk_ASN1_OBJECT_value(stack, i);
+        int nid = OBJ_obj2nid(obj);
+        const char *sn = OBJ_nid2sn(nid);
+        
+        if (sn) 
+        {
+            PyList_SetItem(list, i, PyString_FromString(sn));
+        }
+        else 
+        {
+            PyErr_SetString(PyExc_RuntimeError, "Unknown name for nid");
+            Py_DECREF(list);
+            return NULL;
+        }
+    }
+
+    if (PyList_Sort(list)) 
+    {
+        PyErr_SetString(PyExc_RuntimeError, "Failed to sort list");
+        Py_DECREF(list);
+        return NULL;
+    }
+
+    return list;
+}
+
+static char crypto_X509_get_trusted_uses_doc[] = "\n\
+Return a list of the trusted uses for the certificate.\n\
+\n\
+ at return: A list of strings, or None if the cert contains no trust information.\n\
+";
+
+static PyObject *
+crypto_X509_get_trusted_uses(crypto_X509Obj *self, PyObject *args)
+{
+    if (!PyArg_ParseTuple(args, ":get_trusted_uses"))
+        return NULL;
+
+    if (self->x509->aux && self->x509->aux->trust) 
+    {
+        return crypto_X509_stack_to_list(self->x509->aux->trust);
+    }
+    else 
+    {
+        Py_INCREF(Py_None);
+        return Py_None;
+    }
+}
+
+static char crypto_X509_get_rejected_uses_doc[] = "\n\
+Return a list of rejected uses for the certificate.\n\
+\n\
+ at return: A list of strings, or None if the cert contains no trust information.\n\
+";
+static PyObject *
+crypto_X509_get_rejected_uses(crypto_X509Obj *self, PyObject *args)
+{
+    if (!PyArg_ParseTuple(args, ":get_rejected_uses"))
+        return NULL;
+
+    if (self->x509->aux && self->x509->aux->reject) 
+    {
+        return crypto_X509_stack_to_list(self->x509->aux->reject);
+    }
+    else 
+    {
+        Py_INCREF(Py_None);
+        return Py_None;
+    }
+}
+
+static PyObject *
+crypto_set_trust_or_reject(X509 *x509, PyObject *list, int trust)
+{
+    Py_ssize_t i, n;
+    int *nids;
+
+    n = PyList_Size(list);
+    nids = malloc(n * sizeof *nids);
+
+    for (i = 0; i < n; i++) 
+    {
+        PyObject *o = PyList_GET_ITEM(list, i);
+
+        if (!PyString_Check(o)) 
+        {
+            free(nids);
+            PyErr_SetString(PyExc_TypeError, "List must be of strings");
+            return NULL;
+        }
+
+        nids[i] = OBJ_sn2nid(PyString_AS_STRING(o));
+        if (nids[i] == NID_undef) 
+        {
+            free(nids);
+            PyErr_SetString(PyExc_ValueError, "Trust type not known");
+            return NULL;
+        }
+    }
+
+    X509_trust_clear(x509);
+
+    for (i = 0; i < n; i++) 
+    {
+        ASN1_OBJECT *obj = OBJ_nid2obj(nids[i]);
+        
+        if (trust)
+            X509_add1_trust_object(x509, obj);
+        else
+            X509_add1_reject_object(x509, obj);
+    }
+
+    free(nids);
+
+    Py_INCREF(Py_None);
+    return Py_None;
+}
+                           
+static char crypto_X509_set_trusted_uses_doc[] = "\n\
+Set the list of trusted uses for the certificate.\n\
+\n\
+ at return: None\
+";
+static PyObject *
+crypto_X509_set_trusted_uses(crypto_X509Obj *self, PyObject *args)
+{
+    PyObject *list;
+
+    if (!PyArg_ParseTuple(args, "O!:set_trusted_uses", 
+                          &PyList_Type, &list))
+        return NULL;
+
+    return crypto_set_trust_or_reject(self->x509, list, 1);
+}
+
+static char crypto_X509_set_rejected_uses_doc[] = "\n\
+Set the list of rejected uses for the certificate.\n\
+\n\
+ at return: None\
+";
+static PyObject *
+crypto_X509_set_rejected_uses(crypto_X509Obj *self, PyObject *args)
+{
+    PyObject *list;
+
+    if (!PyArg_ParseTuple(args, "O!:set_rejected_uses", 
+                          &PyList_Type, &list))
+        return NULL;
+
+    return crypto_set_trust_or_reject(self->x509, list, 0);
+}
+
 /*
  * ADD_METHOD(name) expands to a correct PyMethodDef declaration
  *   {  'name', (PyCFunction)crypto_X509_name, METH_VARARGS }
@@ -718,6 +883,10 @@
     ADD_METHOD(subject_name_hash),
     ADD_METHOD(digest),
     ADD_METHOD(add_extensions),
+    ADD_METHOD(get_trusted_uses),
+    ADD_METHOD(get_rejected_uses),
+    ADD_METHOD(set_trusted_uses),
+    ADD_METHOD(set_rejected_uses),
     { NULL, NULL }
 };
 #undef ADD_METHOD

=== modified file 'test/test_crypto.py'
--- test/test_crypto.py	2010-02-11 14:22:24 +0000
+++ test/test_crypto.py	2010-04-20 15:31:37 +0000
@@ -17,6 +17,9 @@
 from OpenSSL.crypto import load_certificate, load_privatekey
 from OpenSSL.crypto import FILETYPE_PEM, FILETYPE_ASN1, FILETYPE_TEXT
 from OpenSSL.crypto import dump_certificate, load_certificate_request
+from OpenSSL.crypto import load_trusted_certificate, dump_trusted_certificate
+from OpenSSL.crypto import TRUST_SSL_CLIENT, TRUST_SSL_SERVER
+from OpenSSL.crypto import TRUST_EMAIL, TRUST_OBJECT_SIGN
 from OpenSSL.crypto import dump_certificate_request, dump_privatekey
 from OpenSSL.crypto import PKCS7Type, load_pkcs7_data
 from OpenSSL.crypto import PKCS12Type, load_pkcs12, PKCS12
@@ -77,6 +80,23 @@
 -----END CERTIFICATE-----
 """
 
+server_trusted_cert_pem = """-----BEGIN TRUSTED CERTIFICATE-----
+MIICKDCCAZGgAwIBAgIJAJn/HpR21r/8MA0GCSqGSIb3DQEBBQUAMFgxCzAJBgNV
+BAYTAlVTMQswCQYDVQQIEwJJTDEQMA4GA1UEBxMHQ2hpY2FnbzEQMA4GA1UEChMH
+VGVzdGluZzEYMBYGA1UEAxMPVGVzdGluZyBSb290IENBMCIYDzIwMDkwMzI1MTIz
+NzUzWhgPMjAxNzA2MTExMjM3NTNaMBgxFjAUBgNVBAMTDWxvdmVseSBzZXJ2ZXIw
+gZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAL6m+G653V0tpBC/OKl22VxOi2Cv
+lK4TYu9LHSDP9uDVTe7V5D5Tl6qzFoRRx5pfmnkqT5B+W9byp2NU3FC5hLm5zSAr
+b45meUhjEJ/ifkZgbNUjHdBIGP9MAQUHZa5WKdkGIJvGAvs8UzUqlr4TBWQIB24+
+lJ+Ukk/CRgasrYwdAgMBAAGjNjA0MB0GA1UdDgQWBBS4kC7Ij0W1TZXZqXQFAM2e
+gKEG2DATBgNVHSUEDDAKBggrBgEFBQcDATANBgkqhkiG9w0BAQUFAAOBgQBh30Li
+dJ+NlxIOx5343WqIBka3UbsOb2kxWrbkVCrvRapCMLCASO4FqiKWM+L0VDBprqIp
+2mgpFQ6FHpoIENGvJhdEKpptQ5i7KaGhnDNTfdy3x1+h852G99f1iyj0RmbuFcM8
+uzujnS8YXWvM7DM1Ilozk4MzPug8jzFp5uhKCTAiMBQGCCsGAQUFBwMCBggrBgEF
+BQcDAaAKBggrBgEFBQcDAw==
+-----END TRUSTED CERTIFICATE-----
+"""
+
 server_key_pem = """-----BEGIN RSA PRIVATE KEY-----
 MIICWwIBAAKBgQC+pvhuud1dLaQQvzipdtlcTotgr5SuE2LvSx0gz/bg1U3u1eQ+
 U5eqsxaEUceaX5p5Kk+QflvW8qdjVNxQuYS5uc0gK2+OZnlIYxCf4n5GYGzVIx3Q
@@ -1427,6 +1447,36 @@
         good_text = _runopenssl(dumped_pem, "x509", "-noout", "-text")
         self.assertEqual(dumped_text, good_text)
 
+    def test_trusted_certificates(self):
+        """
+        L{load_trusted_certificate} test.
+        """
+        pemData = server_trusted_cert_pem
+        cert = load_trusted_certificate(FILETYPE_PEM, pemData)
+        self.assertTrue(isinstance(cert, X509Type))
+        dumped_pem = dump_trusted_certificate(FILETYPE_PEM, cert)
+        good_pem = _runopenssl(dumped_pem, "x509", "-outform", "PEM", "-trustout")
+        self.assertEqual(dumped_pem, good_pem)
+        self.assertEqual(dumped_pem, pemData)
+        # Test equivalence of trusted cert with normal cert
+        plaincert = load_certificate(FILETYPE_PEM, server_cert_pem)
+        self.assertEqual(plaincert.get_subject(), cert.get_subject())
+        trusted_uses = [TRUST_SSL_CLIENT, TRUST_SSL_SERVER]
+        trusted_uses.sort()
+        rejected_uses = [TRUST_OBJECT_SIGN]
+        self.assertEqual(cert.get_trusted_uses(), trusted_uses)
+        self.assertEqual(cert.get_rejected_uses(), rejected_uses)
+        self.assertEqual(plaincert.get_trusted_uses(), None)
+        self.assertEqual(plaincert.get_rejected_uses(), None)
+
+        trusted_uses = [TRUST_SSL_CLIENT]
+        cert.set_trusted_uses(trusted_uses)
+        self.assertEqual(cert.get_trusted_uses(), trusted_uses)
+
+        dump_pem2 = dump_trusted_certificate(FILETYPE_PEM, cert)
+        cert2 = load_trusted_certificate(FILETYPE_PEM, dump_pem2)
+
+        self.assertEqual(cert.get_trusted_uses(), trusted_uses)
 
     def test_dump_privatekey(self):
         """

# Begin bundle
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


More information about the pyopenssl-users mailing list