Merge lp:~mmzeeman/pyopenssl/pkcs7-extensions into lp:~exarkun/pyopenssl/trunk

Proposed by M-MZ
Status: Work in progress
Proposed branch: lp:~mmzeeman/pyopenssl/pkcs7-extensions
Merge into: lp:~exarkun/pyopenssl/trunk
Diff against target: 390 lines
3 files modified
src/crypto/crypto.c (+103/-0)
src/crypto/pkcs7.c (+173/-0)
test/test_crypto.py (+39/-1)
To merge this branch: bzr merge lp:~mmzeeman/pyopenssl/pkcs7-extensions
Reviewer Review Type Date Requested Status
Ziga Seilnacht (community) Needs Fixing
Jean-Paul Calderone Pending
Review via email: mp+13276@code.launchpad.net
To post a comment you must log in.
Revision history for this message
M-MZ (mmzeeman) wrote :

Hello,

Over the past week I have been busy adding some missing functionality to the PKCS7 module. I have added functions to sign and verify pkcs7 structures.

Kind regards,

Maas-Maarten

Revision history for this message
Ziga Seilnacht (zseil) wrote :

Hello,

I'm not sure if I'm the right person for reviewing this, but
here it goes:

1. X509Obj_Sequence_2_Stack:
 - `stack` should be freed in the error branch of the for loop.
 - `sk_X509_push` can fail in low memory conditions. Its return
   value should be checked and appropriate action taken.

2. crypto_pkcs7_sign:
 - What happens if the PKey key contains only a public key or
   no key at all? It would be nice if this case was tested.

3. X509Stack_2_Sequence:
 - Both `X509_dup` and `crypto_X509_New` can fail in low memory
   conditions. An appropriate action should be taken in that case.

4. crypto_PKCS7_get0_signers:
 - `signer_certs` don't get freed if the list was created without
   errors. This looks wrong to me.
 - Why are `signer_certs` freed with `sk_X509_pop_free` instead
   of the more conventional `sk_X509_free`?
 - I don't like the name of this method. Is there a good reason
   for 0 in the name, apart from compatibility with the original
   OpenSSL name?

5. Documentation:
 - Docstrings should describe the required parameters and their
   types, preferably in epydoc format.
 - Latex documentation needs to be written.

6. Tests:
 - TestCase methods should have docstrings explaining what they
   are testing.
 - It would be good if there were some tests against "known good"
   serialized data, either stored as a string in the module or
   generated by spawning openssl.

I'm not qualified to say anything about the API itself, since I
find the win32 API intriguing :)

Thanks for working on this!

Regards,
Ziga

review: Needs Fixing
Revision history for this message
M-MZ (mmzeeman) wrote :

Thanks for reviewing.

>
> I'm not sure if I'm the right person for reviewing this, but
> here it goes:
>

Didn't hear anything from anybody else, so thanks.

> 1, 2, 3

You're right about the low memory stuff.

> 4. crypto_PKCS7_get0_signers:
> - `signer_certs` don't get freed if the list was created without
> errors. This looks wrong to me.
> - Why are `signer_certs` freed with `sk_X509_pop_free` instead
> of the more conventional `sk_X509_free`?
> - I don't like the name of this method. Is there a good reason
> for 0 in the name, apart from compatibility with the original
> OpenSSL name?

It's a long time ago since I programmed this. I will take a look into
it. Could be that it is related to the openssl api. Did't know that
sk_X509_free was the more conventional method.

I indeed took the name to follow the openssl api names. The 0 here
means the direct signers of the data.

> 5, 6

Yes indeed

> I'm not qualified to say anything about the API itself, since I
> find the win32 API intriguing :)

Who doesn't... ;) What I did is follow the openssl api closely as that
is what people might know. There is indeed some room for improvement
here (mildly stated).

Again, thanks for the review.

Maas

Unmerged revisions

127. By M-MZ

Added get_certificates call to pkcs7 objects

126. By M-MZ

Added pkcs7 get0_signers method

125. By M-MZ

Added verify method

124. By M-MZ

Added pkcs7 sign and pkcs7_get_data

123. By M-MZ

Added get_data, no tests yet

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/crypto/crypto.c'
2--- src/crypto/crypto.c 2009-07-17 17:50:12 +0000
3+++ src/crypto/crypto.c 2009-10-13 12:45:19 +0000
4@@ -483,6 +483,103 @@
5 return (PyObject *)crypto_PKCS7_New(pkcs7, 1);
6 }
7
8+static STACK_OF(X509) *
9+X509Obj_Sequence_2_Stack(PyObject *sequence)
10+{
11+ PyObject *e, *item;
12+ int i, nr_of_extensions;
13+ STACK_OF(X509) *stack;
14+
15+ /* Check and create a tuple to get the items */
16+ if(!(e = PySequence_Fast(sequence, "Expected a sequence"))) {
17+ return NULL;
18+ }
19+
20+ /* Make a STACK_OF(X509_EXTENSION) from sequence */
21+ if(!(stack = sk_X509_new_null())) {
22+ exception_from_error_queue(crypto_Error);
23+ return NULL;
24+ }
25+
26+ /* Put the extensions in a stack */
27+ nr_of_extensions = PySequence_Length(e);
28+
29+ for(i = 0; i < nr_of_extensions; i++) {
30+ item = PySequence_Fast_GET_ITEM(e, i);
31+ if(!(crypto_X509_Check(item))) {
32+ PyErr_SetString(PyExc_ValueError,
33+ "One of the elements is not a X509 object");
34+ Py_DECREF(e);
35+ return NULL;
36+ }
37+ sk_X509_push(stack, ((crypto_X509Obj *)item)->x509);
38+ }
39+ Py_DECREF(e);
40+
41+ return stack;
42+}
43+
44+
45+static char crypto_pkcs7_sign_doc[] = "\n\
46+Sign data and embed it in a pkcs7 object\n\
47+\n\
48+@return: The PKCS7 object\n\
49+";
50+static PyObject *
51+crypto_pkcs7_sign(PyObject *spam, PyObject *args)
52+{
53+ PKCS7 *p7;
54+ crypto_PKeyObj *key;
55+ crypto_X509Obj *cert;
56+ PyObject *chain = NULL;
57+ STACK_OF(X509) *stack = NULL;
58+ char *buffer;
59+ int len;
60+ int flags = -1;
61+ BIO *bio;
62+
63+ if(!PyArg_ParseTuple(args, "O!O!s#|Oi:pkcs7_sign",
64+ &crypto_X509_Type, &cert,
65+ &crypto_PKey_Type, &key,
66+ &buffer, &len,
67+ &chain, &flags))
68+ return NULL;
69+
70+ /* Set the chain to NULL if None is given */
71+ if(chain == Py_None)
72+ chain = NULL;
73+
74+ if(chain) {
75+ if(!(stack = X509Obj_Sequence_2_Stack(chain)))
76+ return NULL;
77+ }
78+
79+ if(flags == -1) {
80+ flags = PKCS7_BINARY | PKCS7_NOSMIMECAP;
81+ }
82+
83+ if(!(bio = BIO_new_mem_buf(buffer, len))) {
84+ PyErr_SetString(PyExc_MemoryError, "Could not allocate bio.");
85+ return NULL;
86+ }
87+
88+ if(!(p7 = PKCS7_sign(cert->x509, key->pkey, stack, bio, flags))) {
89+ exception_from_error_queue(crypto_Error);
90+ if(stack)
91+ sk_X509_free(stack);
92+ BIO_free(bio);
93+ return NULL;
94+ }
95+
96+ if(stack)
97+ sk_X509_free(stack);
98+
99+ BIO_free(bio);
100+
101+ return (PyObject *)crypto_PKCS7_New(p7, 1);
102+}
103+
104+
105 static char crypto_load_pkcs12_doc[] = "\n\
106 Load a PKCS12 object from a buffer\n\
107 \n\
108@@ -556,6 +653,7 @@
109 { "load_certificate_request", (PyCFunction)crypto_load_certificate_request, METH_VARARGS, crypto_load_certificate_request_doc },
110 { "dump_certificate_request", (PyCFunction)crypto_dump_certificate_request, METH_VARARGS, crypto_dump_certificate_request_doc },
111 { "load_pkcs7_data", (PyCFunction)crypto_load_pkcs7_data, METH_VARARGS, crypto_load_pkcs7_data_doc },
112+ { "pkcs7_sign", (PyCFunction)crypto_pkcs7_sign, METH_VARARGS, crypto_pkcs7_sign_doc },
113 { "load_pkcs12", (PyCFunction)crypto_load_pkcs12, METH_VARARGS, crypto_load_pkcs12_doc },
114 { "X509_verify_cert_error_string", (PyCFunction)crypto_X509_verify_cert_error_string, METH_VARARGS, crypto_X509_verify_cert_error_string_doc },
115 { "_exception_from_error_queue", (PyCFunction)crypto_exception_from_error_queue, METH_NOARGS, crypto_exception_from_error_queue_doc },
116@@ -671,6 +769,11 @@
117 PyModule_AddIntConstant(module, "FILETYPE_ASN1", X509_FILETYPE_ASN1);
118 PyModule_AddIntConstant(module, "FILETYPE_TEXT", X509_FILETYPE_TEXT);
119
120+ PyModule_AddIntConstant(module, "PKCS7_DETACHED", PKCS7_DETACHED);
121+ PyModule_AddIntConstant(module, "PKCS7_BINARY", PKCS7_BINARY);
122+ PyModule_AddIntConstant(module, "PKCS7_NOSMIMECAP", PKCS7_NOSMIMECAP);
123+ PyModule_AddIntConstant(module, "PKCS7_NOCERTS", PKCS7_NOCERTS);
124+
125 PyModule_AddIntConstant(module, "TYPE_RSA", crypto_TYPE_RSA);
126 PyModule_AddIntConstant(module, "TYPE_DSA", crypto_TYPE_DSA);
127
128
129=== modified file 'src/crypto/pkcs7.c'
130--- src/crypto/pkcs7.c 2009-06-27 18:32:07 +0000
131+++ src/crypto/pkcs7.c 2009-10-13 12:45:19 +0000
132@@ -83,6 +83,40 @@
133 return PyInt_FromLong(0L);
134 }
135
136+static PyObject *
137+X509Stack_2_Sequence(STACK_OF(X509) *stack)
138+{
139+ PyObject *list;
140+ X509 *cert;
141+ crypto_X509Obj *wrapped_cert;
142+ int i;
143+
144+ if(!(list = PyList_New(sk_X509_num(stack)))) {
145+ PyErr_SetString(PyExc_MemoryError,
146+ "Could not allocate memory for list");
147+ return NULL;
148+ }
149+
150+ /*
151+ * Convert the stack to a sequence with crypto_X509Obj's
152+ */
153+ for(i = 0; i < sk_X509_num(stack); i++) {
154+ cert = sk_X509_value(stack, i);
155+
156+ /* Wrap the "bare" certificate, a copy will be made, this
157+ * will make it possible to destroy the pkcs7 structure
158+ * without influencing the wrapped certificates.
159+ */
160+ wrapped_cert = crypto_X509_New(X509_dup(cert), 1);
161+
162+ /* And insert it into the list
163+ */
164+ PyList_SetItem(list, i, (PyObject *)wrapped_cert);
165+ }
166+
167+ return list;
168+}
169+
170 static char crypto_PKCS7_get_type_name_doc[] = "\n\
171 Returns the type name of the PKCS7 structure\n\
172 \n\
173@@ -101,6 +135,141 @@
174 return PyString_FromString(OBJ_nid2sn(OBJ_obj2nid(self->pkcs7->type)));
175 }
176
177+static char crypto_PKCS7_get0_signers_doc[] ="\n\
178+Retrieve the certificates of the signers of this PKCS7 structure\n\
179+\n\
180+@return: A list with X509 certificates\n\
181+";
182+
183+static PyObject *
184+crypto_PKCS7_get0_signers(crypto_PKCS7Obj *self, PyObject *args)
185+{
186+ STACK_OF(X509) *signer_certs;
187+ PyObject *list;
188+
189+ if(!PyArg_ParseTuple(args, ":get0_signers"))
190+ return NULL;
191+
192+ if(!(signer_certs = PKCS7_get0_signers(self->pkcs7, NULL, 0))) {
193+ exception_from_error_queue(crypto_Error);
194+ return NULL;
195+ }
196+
197+ list = X509Stack_2_Sequence(signer_certs);
198+
199+ if(!list) {
200+ sk_X509_pop_free(signer_certs, X509_free);
201+ return NULL;
202+ }
203+
204+ return list;
205+}
206+
207+static char crypto_PKCS7_get_certificates_doc[] = "\n\
208+Retrieve all certificates from the PKCS7 structure\n\
209+\n\
210+@return: A list with X509 certificates\n\
211+";
212+
213+static PyObject *
214+crypto_PKCS7_get_certificates(crypto_PKCS7Obj *self, PyObject *args)
215+{
216+ STACK_OF(X509) *certs;
217+
218+ if(!PyArg_ParseTuple(args, ":get_certificates"))
219+ return NULL;
220+
221+ switch(OBJ_obj2nid(self->pkcs7->type)) {
222+ case NID_pkcs7_signed:
223+ certs = self->pkcs7->d.sign->cert;
224+ break;
225+ case NID_pkcs7_signedAndEnveloped:
226+ certs = self->pkcs7->d.signed_and_enveloped->cert;
227+ break;
228+ default:
229+ PyErr_SetString(PyExc_ValueError,
230+ "It is only possible to get certificates from a signed pkcs7 object");
231+ return NULL;
232+ }
233+
234+ return X509Stack_2_Sequence(certs);
235+}
236+
237+static char crypto_PKCS7_verify_doc[] = "\n\
238+Verifies the signature(s) in the PKCS7 structure\n\
239+\n\
240+@return True if the signatures are ok\n\
241+";
242+
243+static PyObject *
244+crypto_PKCS7_verify(crypto_PKCS7Obj *self, PyObject *args)
245+{
246+ int flags = 0;
247+ int indata_len = 0;
248+ unsigned char *indata = NULL;
249+ BIO *inbio = NULL;
250+ int vr;
251+
252+ if(!PyArg_ParseTuple(args, "|s#i:verify", &indata, &indata_len, &flags))
253+ return NULL;
254+
255+ /*
256+ * Call openssl's verify, but without verifying the certificates.
257+ * This can be done in python.
258+ */
259+ flags |= PKCS7_NOVERIFY;
260+
261+ if(indata_len > 0) {
262+ if(!(inbio = BIO_new_mem_buf(indata, indata_len))) {
263+ PyErr_SetString(PyExc_MemoryError, "Could not allocate bio.");
264+ return NULL;
265+ }
266+ }
267+
268+ vr = PKCS7_verify(self->pkcs7, NULL, NULL, inbio, NULL, flags);
269+ if(inbio) {
270+ BIO_free(inbio);
271+ }
272+
273+ if(vr != 1) {
274+ exception_from_error_queue(crypto_Error);
275+ return NULL;
276+ }
277+
278+ Py_INCREF(Py_True);
279+ return Py_True;
280+}
281+
282+static char crypto_PKCS7_get_data_doc[] = "\n\
283+Returns the data embedded in the PKCS7 structure\n\
284+\n\
285+@return: A string with the embedded data\n\
286+";
287+static PyObject *
288+crypto_PKCS7_get_data(crypto_PKCS7Obj *self, PyObject *args)
289+{
290+ BIO *bio;
291+ char *buf = NULL;
292+ int len = 0;
293+ PyObject *data;
294+
295+ if(!PyArg_ParseTuple(args, ":get_data"))
296+ return NULL;
297+
298+ bio = PKCS7_dataInit(self->pkcs7, NULL);
299+ if(!bio) {
300+ PyErr_SetString(PyExc_ValueError,
301+ "PKCS#7 structure does not have embedded data.");
302+ return NULL;
303+ }
304+
305+ len = BIO_get_mem_data(bio, &buf);
306+ data = PyString_FromStringAndSize(buf, len);
307+ BIO_free_all(bio);
308+
309+ return data;
310+}
311+
312 /*
313 * ADD_METHOD(name) expands to a correct PyMethodDef declaration
314 * { 'name', (PyCFunction)crypto_PKCS7_name, METH_VARARGS }
315@@ -115,6 +284,10 @@
316 ADD_METHOD(type_is_signedAndEnveloped),
317 ADD_METHOD(type_is_data),
318 ADD_METHOD(get_type_name),
319+ ADD_METHOD(get0_signers),
320+ ADD_METHOD(get_certificates),
321+ ADD_METHOD(verify),
322+ ADD_METHOD(get_data),
323 { NULL, NULL }
324 };
325 #undef ADD_METHOD
326
327=== modified file 'test/test_crypto.py'
328--- test/test_crypto.py 2009-08-27 16:47:55 +0000
329+++ test/test_crypto.py 2009-10-13 12:45:19 +0000
330@@ -11,6 +11,7 @@
331 from datetime import datetime, timedelta
332
333 from OpenSSL.crypto import TYPE_RSA, TYPE_DSA, Error, PKey, PKeyType
334+from OpenSSL.crypto import PKCS7_DETACHED
335 from OpenSSL.crypto import X509, X509Type, X509Name, X509NameType
336 from OpenSSL.crypto import X509Req, X509ReqType
337 from OpenSSL.crypto import X509Extension, X509ExtensionType
338@@ -18,7 +19,7 @@
339 from OpenSSL.crypto import FILETYPE_PEM, FILETYPE_ASN1, FILETYPE_TEXT
340 from OpenSSL.crypto import dump_certificate, load_certificate_request
341 from OpenSSL.crypto import dump_certificate_request, dump_privatekey
342-from OpenSSL.crypto import PKCS7Type, load_pkcs7_data
343+from OpenSSL.crypto import PKCS7Type, load_pkcs7_data, pkcs7_sign
344 from OpenSSL.crypto import PKCS12Type, load_pkcs12, PKCS12
345 from OpenSSL.crypto import NetscapeSPKI, NetscapeSPKIType
346 from OpenSSL.test.util import TestCase
347@@ -1509,6 +1510,43 @@
348 # XXX This doesn't currently work.
349 # self.assertIdentical(PKCS7, PKCS7Type)
350
351+ def test_pkcs7_sign_and_verify(self):
352+ root_cert = load_certificate(FILETYPE_PEM, root_cert_pem)
353+ root_key = load_privatekey(FILETYPE_PEM, root_key_pem)
354+ pkcs7 = pkcs7_sign(root_cert, root_key, "Hello world")
355+ self.assertTrue(pkcs7)
356+
357+ self.assertEquals("Hello world", pkcs7.get_data())
358+ self.assertEquals(True, pkcs7.verify())
359+
360+ # Too bad, no direct compare possible.
361+ self.assertEquals(root_cert.get_serial_number(),
362+ pkcs7.get0_signers()[0].get_serial_number())
363+
364+ def test_pkcs7_sign_and_verify_detached(self):
365+ root_cert = load_certificate(FILETYPE_PEM, root_cert_pem)
366+ root_key = load_privatekey(FILETYPE_PEM, root_key_pem)
367+ pkcs7 = pkcs7_sign(root_cert, root_key, "Hello world", [], PKCS7_DETACHED)
368+ self.assertEquals("", pkcs7.get_data())
369+ self.assertEquals(True, pkcs7.verify("Hello world", PKCS7_DETACHED))
370+ self.assertRaises(Error, pkcs7.verify, "Bad data", PKCS7_DETACHED)
371+
372+ def test_pkcs7_get_certificates(self):
373+ root_cert = load_certificate(FILETYPE_PEM, root_cert_pem)
374+ server_cert = load_certificate(FILETYPE_PEM, server_cert_pem)
375+ server_key = load_privatekey(FILETYPE_PEM, server_key_pem)
376+
377+ pkcs7 = pkcs7_sign(server_cert, server_key, "Hello world", [], PKCS7_DETACHED)
378+ self.assertEquals(1, len(pkcs7.get0_signers()))
379+ self.assertEquals(1, len(pkcs7.get_certificates()))
380+
381+ pkcs7 = pkcs7_sign(server_cert, server_key, "Hello world", [root_cert])
382+ self.assertEquals(1, len(pkcs7.get0_signers()))
383+ self.assertEquals(2, len(pkcs7.get_certificates()))
384+
385+ def test_get_data(self):
386+ pkcs7 = load_pkcs7_data(FILETYPE_PEM, pkcs7Data)
387+ self.assertRaises(ValueError, pkcs7.get_data)
388
389
390 class NetscapeSPKITests(TestCase):

Subscribers

People subscribed via source and target branches

to status/vote changes: