From jorton at redhat.com Tue Apr 20 17:42:40 2010 From: jorton at redhat.com (Joe Orton) Date: Tue, 20 Apr 2010 16:42:40 +0100 Subject: [pyOpenSSL] [PATCH] add support for handling trusted certificates Message-ID: <20100420154240.GA12861@redhat.com> 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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