[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
IyBCYXphYXIgcmV2aXNpb24gYnVuZGxlIHY0CiMKQlpoOTFBWSZTWbDkt/YAC6v/gGRwAYB5f///
f////r////pgFj30dnztSV3u9b32297557h1trtnQJtw+9cvTJntrQw3cH3test9g3m9I2Ybao7t
S5tScAy3UMJJImmRNkBNMIwTATTTBNTJgZJtTQmBIHqaaCUgTQNGhDEE0ymxQyn6GlB6jTTQaaG1
NAAyAJSATainhU9TT9U3ommKDQ9QDTTI9RkaPUAAAACQkEQjRlT9E0NDRJo0ZNH6hDJ6QAbSA0aB
mp6CJSTKek1Hpon5INRppP0JHk0NGmRoaaI9ATBBoGmCSIQBGgCZTwk2mJpKfqanjQU9Nqeqep5T
eoag9T1NAep6miQkcAaSBZyTh7O588jGxsG2wbT8M9RjeLjnjznh9PH0bn4Kn0ujDTc4zfSn9Tdx
IDsxv4Pw45uS6kPmyyFNHBnsW9yAwmty3ayw0F1HBtsGeI5MGma2zcbEdntZ0XiZdnujNPq9WcWU
mx4q1ecc1/7dKyMEL6RKTnC4rs5iT+2Yiw0Ui0PCQprFuil+54kpBDsv2vAkQM+lw8jo1ervpL32
cPawrfG1aEeYBgX0zLB0bsWL46sbfWi0mOVqg3CTNFyWau09P1pX+66SbXJjqcXoaNWsCY3DBuEg
GoOoQkc5t4FUBwigPDF3K0CDu0JH/yFkHvLvfaoqvn0LNtmrgyET+zRAx4fhVszj3KD7jmn/sOBt
ZoUF6Hd+6eU70aHAKQwbE22LNjV91ySMZNY46aYjZvaTEB1gmfpZpc7wjxJzk9q1xRTDFsOru70a
qUuZfF8IqVfkOqdl+ku7dc/+qXvO47Q/1NVGltyyr8TTnNppME8VOMFYlBc5gIkCQYFSnR4gJAPQ
t9kRW+22/FmO+33MFsWRYi918N5t+XTQ3BCF+olvXDOBbIqbRjHEX2Qzy6m2bSqdN0b9m+5Rt0Z8
bFPCHr/ArPM+BQaeDSxaBSqqZPEnIO5C6PgO8pFtSg09pBSPxU3JES6w7PIbtTHt+GxmxGm7j01R
RgZAP4E37cbaOHNR3SDKEssr7nOrEFFKEIjHk08BzrpFKXFFjU0EoVMJr1gzrglIsebLk2dIzNIT
C3oIYGhUuJR9tRplSNatxgeqaBvmZobGOiOnMHZmb593G28b0JjALPmzxRIYSZJpCASMYXaegHZK
z1kAMpqWFIH6Z67kjwCw5zSU8zRSNW//LN7ddVE/lBi0IqC7R0Rraw13RFcmXJah26ftTfI00k0c
MSDqkc0egYsyx7JKdhcXq0GhuiCyejdo6zfPoscagfsLujqrxf1792McxDnfmjttCkk4OgyUW6WP
NT03uBlDWWsnCzK829VryqNWBQo+RWMsOSs0wfIX9IVJ5ZA7BtybKf9ZyEGvlVNW8X5PvHV9/ID8
BXIIdgyUKihmyiM67h1dfYznHg7Mt7jMUsGUXUiS4Bd3O3YKkqtf9iFlb9DDcxQuLhgpROmNKPmD
NAP2LutvIfW6MnsDhDKXcxkHIGsi8olB0G0Mou0KlsAu2zh93Upl8RHYs1AsKfVPVRAku3Zmkmup
nHhWIUmktvFKFHHuUG9IgTABIHJlw9Nya6DcpDLHk3GnJedL8bFrWta0jI9WHoF/Wu8iAo0pft8q
sGJGGBoAukZXuIMUJBoxKNBehQRXkDpExqU6QAzZ7exeEwXNdbWQYokozQFfKYCTgRciIFpSiDQc
iVp+uHsca1N39rYSo7cE9yioXDLpIiPuQf0zLFRiErmSxkgc95yuDirBIMExHeLj1FyzyCa+FKSn
VCe+JkGs1WK4lLXew65ZYU51guZcwixxqcFlZBIeATtM9hW+a8LsWFaICKRLgq+QZioxoy83hXA4
3E5QlgFlJzUKrEyocv393QD0NZNDFVJfz1DMoRfLYzl5JVuQLkU9lOxIqZCCCAHUkFRINl4JDCzf
GECgJwdhHLZSXFAhLNMGhkatrbkjPQNm5KYNSLi7QRoacyNU8Ev9Hlhp6IcZ6KizewsIbWEz8IWm
5x7dje3s6m6lXjVXc4PpHgzc5tUTj571AjOWa8iCnET5S8qds9VMCEGzEUXe9JFCDCoWCiLR/uxn
g6V48nTP2WiGAjAjeI788XtXLTdZxjCuNJFFpUORLoAsHARW7mUeqRnqIvveaswuIzEHhPIKyjPD
Xc4paA0KRRRNhO8SWcD7fEdfD2vqfSUsYnmr/3LVjerLRraq5T6VrkULyeoPfBBSIgQCXHFc1MKb
abhCmSRssWzRsir66QewQYrMWFJhYOHkk4u7rrkWh2y8orMQUlvydVjw8pf1085jXXKVtZxkCgQe
tHRTOjhUknK4uuhiVGkkFkq+TdA44STRrVWYr1hIuubgbtiWIGzVewxhEjS1bjF7EggPEyTCHxqC
Fz7JkwgtEJ8w9rlpcD7ULCQQdMLaINwjmnxgPVaNepVS7yqgIrgUldiZUQ48/TPdkLqLiYzKgJgG
dELo7CidRFzmOGUrkQKm1BllqJcgyM1Bzcb5yQuDmbHCvFJCwlOOZAsfYSI41hzSMpIuNPJ39OCW
htTU2sTxg2k+gRxwOO8P5uiEdmapsX8yyCwighgOa8RhB8xK8qZGlCmcQXAr415IvpgTCtvFUTZQ
TGfFxoWpVCGX2/9ckgiWDd2yLiBgdFkGGbOuVhCK9+30/MeOpXatp3isRQsJGGXRk4UojTgoROJ+
to3rK5EysCP88lYDKiQ12LYs1sIBtBbdia13n4hm8HDj01R70Z0qchi7v2zJHrQtGwZyGhRMA1NV
rWGY6Vop5l+gBr+7SbExiFRlNcmM7QbI5mNtMptuqBcS6kahdKZJQggnzZyCE0W/G6Gl4y/xf/Yb
fCAlyat5V5Nt1S63V1cRrPYHu+I08TLRpsuRXPv34CQ0Wm5x1+AqM2V9I9dPBaE3yYBwwzD0VWvK
giO3P+6V4K/9FdWfAE16LVbYqXjpdZ2Jq8TqNngQm4Yc9cNltjfJylIUT68xlGycNwc3XclpxzFL
EzKEl4CVdFnf9zymgPHk9D1E0JbQqNXfo5vhx/TdLkCvwWWdy4ooTBgM7ppiAdKG0ltkMVcWiTeq
GQcwxi8rIt7dRuNKfKLkAcLJ60ltG9KDrFfOQUkL4peBkcldw654QmQmjBnQ5Zuy/UzZah5wlgmd
ND7w8MUR2ef23Vn4+viqi6D63z5+WnoKlfnBscN58uZuztt8fXQfKVV+FYSwI8Q7V93ZmIAC9KZ3
ohvZvnv09/0szdv4Z7tfr2u03ZbgNXDvYG931JLQw9l+npXf6fghbRtQVpTrg0RNrgD24gyJBClh
1bUqXf6SBRZLun56/nKPbpHFGSxYtarlxugMhPqZncdh7E/tzUTwgsine7JdYuILEXFKTmagy72t
SPpB8tCrrFhkjKtvCAQExizXzxbiqcCcMLn/Qb19p5CpB7dwvEki/WKWLNWX2QSOgAy9eEkLz/IX
uP2eCILYRz6j6BUBTFG1elixWLwurKvB8np97f4+PY/q0YTxflqHPE+qss3SzES0OHch4wDyAYOu
niGh2QnDnL3hXs+OihKfnEYEFomtKlMlq9g7JKma24kB4tFzC+I+LGD66Det12jOgx7XvG24gM09
+0LlHc8eYhsKMfsZION66gHN6dNFZh0JgM2a9uIL6Ozd5haYwl5KcijeT6QUkBKXbBFK2zb/kz8v
dOHvXJOXlflZ/RoVh8cxMoVK3yy0qXvxR9Jn2tcoiZkyGXYMk5hD/A6cXS9Ft2089J0OwAw87vnE
/7uatBtwVvYqr0F5LQjMMhH1/DtTnugT9bqFUIAn5s+0Xon7AUyvsVAXyB7vJZZIEPlOSlyCRGIv
SpJhr/ZakUiY5ixTdrIPVXkExPG2BRsGVwGJl3MATE1O2EsfpckwCmwjBGQbApRKJLuRrGqFAoGG
incS+aoQU/lKg7zC0ALUzLnLPbcd7B4pwVn58LcdSuBlOjTidF2yQRiNfo7g9zG/IdBwOLbGmNsb
bobSXngIOXK3FLqWm35TzUuXgAc/0Wu9fs+HDt2iGwMJDK6Q5dOnk1xoACa1UC7KYF2jp19WgWE9
bwREtD+tBl+DPO9FEoyRKJDyvWoX0tX9gA+g/iOYJH6FuFMN4dQsAsnYIk2Ms95iBIB5URKs41nM
f2Lwyhr+3twrLueO37mQ30T31JeQI5ZGKWZSk4r6IuwtL3BDwKxdSLw4gqC6kmSQ2cYMsw5F8Uet
yZOmn9OcfeSQeVlqHUjGfg4FAsRPALcVHPSgaF5gLzmxbMZ+9Dr7fteXlxjxDw+aMuJICoybb508
TKVHR/C0s3E17XFGxx1F3cdRHrRdBwmAGBgZlOIzpMOomKpzxMws4gPcwRBgrW4WZX51BVZzezNO
DmEsGDD+mPhWMwzDvI03ZkqqKZXQhC6QZ8b4Pz8YizV7QVwfYFPlUCqSOHSUoOI5QkNs0cScqLSl
Qh8K0Lf2Z+7yJJkQsfHOSGYSqMzJyQwIJ/Ljv7xoDWYHxRVS5HgIa5E+eAHL4hFvxQdVEDlRIMky
FFGxQxuTIaPSse6zmdYDoLFriiI56GSN6PTSYhiJNAp79MGmWUUkQXlRfvXopSa7BvMUmRQImaBU
4FACCFhSZr6Ljs9NOF2mQ6S52zqiLyxtGPI4hftvGyQfxbzE72mQyrDoM6GVuhkZ2ZxneE+h2NCT
0lIqEMjGC4yrxVlluZkUv7gArYERZ/eyubVJ52RaMgUgYEEQNXWsp1rLrmgp1SGcyIMqzTgKNONO
ZHWDid6gKhJG7rGxhpucQ5wzEQYUU36jhIuWRkOdSB0jXcdWwWexSgicLrxoJKCGtyaQESNukEmx
DaDcqVdzERpzYw5yRcxbCSSQkIEkCSSEJYYBDaIMg5yC9CbMZpdkNYxo4WaSBvwiGag657I6UkQW
M6RcDr0HdvmJzWS8hE6h6N6vK8b7bKgm57cR1SgMzJE8n5CccYEiRVEOSA/aMRmQwocNvWoinYNq
ic6JfeUqaJMnmQbkJWMn+bC+C7aWdeZviRkE4ruXhoSWFBdd3j+phyDA8GLVTVQOtPL1YDNx5upU
aIJjKDjfNtlFDJ2Y1o/DI5sH5l6wCe5B9Mq4kzIYZPABmsw/rx/XQgxRE7PW4uTl4xfjCkFNXkwE
KCaH0e9xzruEV9Qj4VM+78qpfIxbVApusi5XfRohRgaVjyHEO/CRh5y4iBJID8/fZsJnA8xyLhA9
HMyCedBEMwLVApAuYS1TE+AwTuNNN7FxVS7kgtVSZMAyZZMgipNsTcwlQvMkJkuBFPcp2lQcnEHl
vJ6UqWCv43b9n379DuqdLEK4mGTMgGuCnGgW+jgLA1tDDhnAw4GTriqiPXUKayxXRFgNyDEiVvp0
slMlB7+Kv0gvQVmZWohfxt14Q3aaaKDI8HK4C4OAosjGu+UDwnJbh6A3alpCDQaJhlBibEwGt7Zc
S3EqdBRn7ZwJ1Qeh2UBj9Aeyu5NeUI8eTlKZj65P27BHdUCoGAnEtZyQNOBBmFzBEHsECvPExiNn
xiQmPWeuYRBRNTjm96coyDwqFoPwbmVZQS0qSpGWG8vAR1ki0Tg0Uw5xRySOGTOQpAGRGLzj7UGB
Xsdc8FVy6iZAvvaZ4kmz0QdqAPgyl0iKRVzDBJCC+G26Sj9hCvDo9gjppS5w93VTnpBlaDOC8sIk
a0BzkwhbVWc5mkcz9jDd7moK6vVCo5/GIgfeMHQHRGkyDDUTgNooZkaCrUUx1IcviDbqMnOPiKjT
KqKQGGXh1/DWBWREHZqBVGjJW1zMMdhMws4ZT1dBuiL8BcVzVHqZBYFowI6mowAgAQe+ZCCd9r6B
j2C3N206lqVgwoCMoWgpxDeAASgyDUuxsr+9uwAI0CJ1VzQ5WyPFp62DzjLF0bYVcM3yxyQogBbi
PyeJqSKVgdIQia6nbrPU3rp4Np0OAYbQZKFdmfMj3FGm3QYCegSvpFW6EKoS7nxudXya1puiP5QU
ShfFs1mqerygHDhMzOYGfHngHj1fciZsr7RHdJdS4uZBMViYYFYu9ZpiIFoxhkjVD7gJSX8iG8fj
QaQcGJQ97+Vfj0FwQgQoQjwDneNsAtADSKoS3471HDyASEjh3UBUkeRAEsgbyGK1zCQ3BcZojB0S
ECQggSCh3GiAem3E0igHHYFYnIgtHxwK+Qp1454UiA4mFIGHEzEoEH4h3vGrodTBbLcqQiqQuF9M
YvNu7gYNrM24RtsEXpHj1gIlGdhO1MHIHwNtsIjLH1JKI1reMgcA4sbTZz/JtNM7nc4Q4qCL+AOb
oC9OFASQGmhtKJpEgNDMajwilgDpEsAJdALEYXwS30/dOXy+NBo0ja1z3t7TjY2oJjd9vaWHYsVJ
ALkjKzzWqiOWFChlEI4wlSNFaPFrBSupikEfUODGZWykGncxTXCWrQxWqkWVAvpEMVDSWgQDAxC4
rWyQsKoG7GA0eo6aBRjbbbabbExjG2Nth6ef1X5ddxOU9kPfSSmxBL2vnE1fAhc0LFNUsb4LeKFd
5qZhrysJrmiRkRSsIhaoGqjaLDU/HszXpEDV9V1v4TAoJoF6BoIrQKZkXRZRTivQSHWJADVmYaw0
jh+vhx0nggtOFhekEogSX8+jh7XYiBaVylN8RmP4gEHmGwhazP+ZrFTOpAElnBkAcFOkgOF9sBM7
i8vn9YvD5d1W63z+euglB5iLoltIQ7+t9BghZ/k+ru/hDbiiDJEV1XKWslsG/Pn1rtYcMLtfZvHD
0Gmnx8beHHPB8TJK3htLG5OSzahcGEAW9IrNwRdooJi1cSWdhkwNOQJPyhc8AfLM6Sd4fPY232ia
zrslIR5ZNAuVUgoAZNgHmC/SDP50RKoiKKQz5D7RdyRThQkLDkt/YA==
More information about the pyopenssl-users
mailing list