reason codes for CRL
diff --git a/doc/pyOpenSSL.tex b/doc/pyOpenSSL.tex
index d43ad96..c7d3a6c 100644
--- a/doc/pyOpenSSL.tex
+++ b/doc/pyOpenSSL.tex
@@ -614,6 +614,15 @@
Revoked objects have the following methods:
+\begin{methoddesc}[Revoked]{all_reasons}{}
+Return a list of all supported reasons.
+\end{methoddesc}
+
+\begin{methoddesc}[Revoked]{get_reason}{}
+Return the revocation reason as a str. Can be
+None, which differs from "Unspecified".
+\end{methoddesc}
+
\begin{methoddesc}[Revoked]{get_rev_date}{}
Return the revocation date as a str.
The string is formatted as an ASN1 GENERALIZEDTIME.
@@ -623,6 +632,12 @@
Return a str containing a hex number of the serial of the revoked certificate.
\end{methoddesc}
+\begin{methoddesc}[Revoked]{set_reason}{reason}
+Set the revocation reason. \var{reason} must
+be None or a string, but the values are limited.
+Spaces and case are ignored. See \method{all_reasons}.
+\end{methoddesc}
+
\begin{methoddesc}[Revoked]{set_rev_date}{date}
Set the revocation date.
The string is formatted as an ASN1 GENERALIZEDTIME.
diff --git a/src/crypto/revoked.c b/src/crypto/revoked.c
index 8289408..99d00e1 100644
--- a/src/crypto/revoked.c
+++ b/src/crypto/revoked.c
@@ -3,6 +3,203 @@
#include "crypto.h"
+/* http://www.openssl.org/docs/apps/x509v3_config.html#CRL_distribution_points_ */
+/* which differs from crl_reasons of crypto/x509v3/v3_enum.c that matches */
+/* OCSP_crl_reason_str. We use the latter, just like the command line program. */
+static const char *crl_reasons[] = {
+ "unspecified",
+ "keyCompromise",
+ "CACompromise",
+ "affiliationChanged",
+ "superseded",
+ "cessationOfOperation",
+ "certificateHold",
+ NULL,
+ "removeFromCRL",
+};
+
+#define NUM_REASONS (sizeof(crl_reasons) / sizeof(char *))
+
+static char crypto_Revoked_all_reasons_doc[] = "\n\
+Return a list of all the supported reason strings.\n\
+\n\
+@return: A list of reason strings.\n\
+";
+static PyObject *
+crypto_Revoked_all_reasons(crypto_RevokedObj *self, PyObject *args)
+{
+ PyObject *list, *str;
+ int j;
+
+ list = PyList_New(0);
+ for (j = 0; j < NUM_REASONS; j++)
+ {
+ if( crl_reasons[j] )
+ {
+ str = PyString_FromString(crl_reasons[j]);
+ PyList_Append(list, str);
+ Py_DECREF(str);
+ }
+ }
+ return list;
+}
+
+static PyObject *
+X509_EXTENSION_value_to_PyString(X509_EXTENSION *ex)
+{
+ BIO *bio = NULL;
+ PyObject *str = NULL;
+ int str_len;
+ char *tmp_str;
+
+ /* Create a openssl BIO buffer */
+ bio = BIO_new(BIO_s_mem());
+ if (bio == NULL)
+ goto err;
+
+ /* These are not the droids you are looking for. */
+ if(!X509V3_EXT_print(bio, ex, 0, 0))
+ if(M_ASN1_OCTET_STRING_print(bio, ex->value) == 0)
+ goto err;
+
+ /* Convert to a Python string. */
+ str_len = BIO_get_mem_data(bio, &tmp_str);
+ str = PyString_FromStringAndSize(tmp_str, str_len);
+
+ /* Cleanup */
+ BIO_free(bio);
+ return str;
+
+ err:
+ if(bio) {
+ BIO_free(bio);
+ }
+ if(str) {
+ Py_DECREF(str);
+ }
+ return NULL;
+}
+
+static void
+delete_reason(STACK_OF(X509_EXTENSION) *sk)
+{
+ X509_EXTENSION * ext;
+ int j;
+
+ for(j = 0; j < sk_X509_EXTENSION_num(sk); j++) {
+ ext = sk_X509_EXTENSION_value(sk, j);
+ if ( OBJ_obj2nid(ext->object) == NID_crl_reason) {
+ X509_EXTENSION_free(ext);
+ (void) sk_X509_EXTENSION_delete(sk, j);
+ break;
+ }
+ }
+}
+
+static int
+reason_str_to_code(const char * reason_str)
+{
+ int reason_code = -1, j;
+ char *spaceless_reason, * sp;
+
+ /* Remove spaces so that the responses of
+ * get_reason() work in set_reason() */
+ if((spaceless_reason = strdup(reason_str)) == NULL)
+ return -1;
+ while((sp = strchr(spaceless_reason, ' ') ))
+ {
+ memmove(sp, sp+1, strlen(sp));
+ }
+
+ for (j = 0; j < NUM_REASONS; j++)
+ {
+ if(crl_reasons[j] && !strcasecmp(spaceless_reason, crl_reasons[j]))
+ {
+ reason_code = j;
+ break;
+ }
+ }
+ free(spaceless_reason);
+ return reason_code;
+}
+
+static char crypto_Revoked_set_reason_doc[] = "\n\
+Set the reason of a Revoked object.\n\
+\n\
+@param reason: The reason string.\n\
+@type reason: L{str}\n\
+@return: None\n\
+";
+static PyObject *
+crypto_Revoked_set_reason(crypto_RevokedObj *self, PyObject *args, PyObject *keywds)
+{
+ static char *kwlist[] = {"reason", NULL};
+ const char *reason_str = NULL;
+ int reason_code;
+ ASN1_ENUMERATED *rtmp = NULL;
+
+ if (!PyArg_ParseTupleAndKeywords(args, keywds, "z:set_reason",
+ kwlist, &reason_str))
+ return NULL;
+
+ if(reason_str == NULL)
+ {
+ delete_reason(self->revoked->extensions);
+ goto done;
+ }
+
+ reason_code = reason_str_to_code(reason_str);
+ if(reason_code == -1)
+ {
+ PyErr_SetString(PyExc_ValueError, "bad reason string");
+ return NULL;
+ }
+
+ rtmp = ASN1_ENUMERATED_new();
+ if (!rtmp || !ASN1_ENUMERATED_set(rtmp, reason_code))
+ goto err;
+ delete_reason(self->revoked->extensions);
+ if (!X509_REVOKED_add1_ext_i2d(self->revoked, NID_crl_reason, rtmp, 0, 0))
+ goto err;
+
+ done:
+ Py_INCREF(Py_None);
+ return Py_None;
+
+ err:
+ exception_from_error_queue(crypto_Error);
+ return NULL;
+}
+
+
+static char crypto_Revoked_get_reason_doc[] = "\n\
+Return the reason of a Revoked object.\n\
+\n\
+@return: The reason as a string\n\
+";
+static PyObject *
+crypto_Revoked_get_reason(crypto_RevokedObj *self, PyObject *args)
+{
+ X509_EXTENSION * ext;
+ int j;
+ STACK_OF(X509_EXTENSION) *sk = NULL;
+
+ if (!PyArg_ParseTuple(args, ":get_reason"))
+ return NULL;
+
+ sk = self->revoked->extensions;
+ for(j = 0; j < sk_X509_EXTENSION_num(sk); j++) {
+ ext = sk_X509_EXTENSION_value(sk, j);
+ if ( OBJ_obj2nid(ext->object) == NID_crl_reason) {
+ return X509_EXTENSION_value_to_PyString(ext);
+ }
+ }
+
+ Py_INCREF(Py_None);
+ return Py_None;
+}
+
+
static char crypto_Revoked_get_rev_date_doc[] = "\n\
Retrieve the revocation date\n\
\n\
@@ -16,9 +213,9 @@
static PyObject*
crypto_Revoked_get_rev_date(crypto_RevokedObj *self, PyObject *args)
{
- /* returns a borrowed reference. */
- return _get_asn1_time(
- ":get_rev_date", self->revoked->revocationDate, args);
+ /* returns a borrowed reference. */
+ return _get_asn1_time(
+ ":get_rev_date", self->revoked->revocationDate, args);
}
static char crypto_Revoked_set_rev_date_doc[] = "\n\
@@ -36,17 +233,19 @@
static PyObject*
crypto_Revoked_set_rev_date(crypto_RevokedObj *self, PyObject *args)
{
- return _set_asn1_time(
- "s:set_rev_date", self->revoked->revocationDate, args);
+ return _set_asn1_time(
+ "s:set_rev_date", self->revoked->revocationDate, args);
}
-
+/* The integer is converted to an upper-case hex string
+ * without a '0x' prefix. */
static PyObject *
ASN1_INTEGER_to_PyString(ASN1_INTEGER *asn1_int)
{
BIO *bio = NULL;
- PyObject *buf = NULL;
- int ret, pending;
+ PyObject *str = NULL;
+ int str_len;
+ char *tmp_str;
/* Create a openssl BIO buffer */
bio = BIO_new(BIO_s_mem());
@@ -54,32 +253,23 @@
goto err;
/* Write the integer to the BIO as a hex string. */
- i2a_ASN1_INTEGER(bio, asn1_int);
-
- /* Allocate a Python string. */
- pending = BIO_pending(bio);
- buf = PyString_FromStringAndSize(NULL, pending);
- if (buf == NULL) {
+ if(i2a_ASN1_INTEGER(bio, asn1_int) < 0)
goto err;
- }
- /* Copy the BIO contents to a Python string. */
- ret = BIO_read(bio, PyString_AsString(buf), pending);
- if (ret <= 0) { /* problem with BIO_read */
- goto err;
- }
+ /* Convert to a Python string. */
+ str_len = BIO_get_mem_data(bio, &tmp_str);
+ str = PyString_FromStringAndSize(tmp_str, str_len);
/* Cleanup */
BIO_free(bio);
- bio = NULL;
- return buf;
+ return str;
err:
if(bio) {
BIO_free(bio);
}
- if(buf) {
- Py_DECREF(buf);
+ if(str) {
+ Py_DECREF(str);
}
return NULL;
}
@@ -125,7 +315,7 @@
return NULL;
if( ! BN_hex2bn(&serial, hex_str) ) {
- PyErr_SetString(PyExc_TypeError, "bad hex string");
+ PyErr_SetString(PyExc_ValueError, "bad hex string");
return NULL;
}
@@ -147,7 +337,7 @@
self = PyObject_New(crypto_RevokedObj, &crypto_Revoked_Type);
if (self==NULL)
- return NULL;
+ return NULL;
self->revoked = revoked;
return self;
}
@@ -163,6 +353,9 @@
{ #name, (PyCFunction)crypto_Revoked_##name, METH_VARARGS | METH_KEYWORDS, crypto_Revoked_##name##_doc }
static PyMethodDef crypto_Revoked_methods[] =
{
+ ADD_METHOD(all_reasons),
+ ADD_METHOD(get_reason),
+ ADD_KW_METHOD(set_reason),
ADD_METHOD(get_rev_date),
ADD_METHOD(set_rev_date),
ADD_METHOD(get_serial),
diff --git a/test/test_crypto.py b/test/test_crypto.py
index 630ce01..641bdaa 100644
--- a/test/test_crypto.py
+++ b/test/test_crypto.py
@@ -1168,6 +1168,7 @@
self.assertEqual( type(revoked), Revoked )
self.assertEqual( revoked.get_serial(), '00' )
self.assertEqual( revoked.get_rev_date(), None )
+ self.assertEqual( revoked.get_reason(), None )
def test_serial(self):
@@ -1186,7 +1187,7 @@
ser = revoked.get_serial()
self.assertEqual( ser, '31' )
- self.assertRaises(TypeError, revoked.set_serial, 'pqrst')
+ self.assertRaises(ValueError, revoked.set_serial, 'pqrst')
self.assertRaises(TypeError, revoked.set_serial, 100)
@@ -1207,6 +1208,34 @@
self.assertEqual( date, now )
+ def test_reason(self):
+ """
+ Confirm we can set and get revocation reasons from
+ L{OpenSSL.crypto.Revoked}. The "get" need to work
+ as "set". Likewise, each reason of all_reasons() must work.
+ """
+ revoked = Revoked()
+ for r in revoked.all_reasons():
+ for x in xrange(2):
+ ret = revoked.set_reason(r)
+ self.assertEqual( ret, None )
+ reason = revoked.get_reason()
+ self.assertEqual( reason.lower().replace(' ',''),
+ r.lower().replace(' ','') )
+ r = reason # again with the resp of get
+
+ revoked.set_reason(None)
+ self.assertEqual(revoked.get_reason(), None)
+
+
+ def test_bad_reasons(self):
+ """
+ Use L{OpenSSL.crypto.Revoked.set_reason} in bad ways.
+ """
+ revoked = Revoked()
+ self.assertRaises(TypeError, revoked.set_reason, 100)
+ self.assertRaises(ValueError, revoked.set_reason, 'blue')
+
class CRLTests(TestCase):
"""
@@ -1236,18 +1265,21 @@
now = datetime.now().strftime("%Y%m%d%H%M%SZ")
revoked.set_rev_date(now)
revoked.set_serial('3ab')
+ revoked.set_reason('sUpErSeDEd')
crl.add_revoked(revoked)
# PEM format
dumped_crl = crl.export(self.cert, self.pkey, days=20)
text = _runopenssl(dumped_crl, "crl", "-noout", "-text")
text.index('Serial Number: 03AB')
+ text.index('Superseded')
text.index('Issuer: /C=US/ST=IL/L=Chicago/O=Testing/CN=Testing Root CA')
# DER format
dumped_crl = crl.export(self.cert, self.pkey, FILETYPE_ASN1)
text = _runopenssl(dumped_crl, "crl", "-noout", "-text", "-inform", "DER")
text.index('Serial Number: 03AB')
+ text.index('Superseded')
text.index('Issuer: /C=US/ST=IL/L=Chicago/O=Testing/CN=Testing Root CA')
# text format
@@ -1255,6 +1287,7 @@
self.assertEqual(text, dumped_text)
+
def test_get_revoked(self):
"""
Use python to create a simple CRL with two revocations.
@@ -1269,6 +1302,7 @@
revoked.set_serial('3ab')
crl.add_revoked(revoked)
revoked.set_serial('100')
+ revoked.set_reason('sUpErSeDEd')
crl.add_revoked(revoked)
revs = crl.get_revoked()
@@ -1289,29 +1323,32 @@
crl_txt = """
-----BEGIN X509 CRL-----
-MIIBTTCBtzANBgkqhkiG9w0BAQQFADBYMQswCQYDVQQGEwJVUzELMAkGA1UECBMC
+MIIBWzCBxTANBgkqhkiG9w0BAQQFADBYMQswCQYDVQQGEwJVUzELMAkGA1UECBMC
SUwxEDAOBgNVBAcTB0NoaWNhZ28xEDAOBgNVBAoTB1Rlc3RpbmcxGDAWBgNVBAMT
-D1Rlc3RpbmcgUm9vdCBDQRcNMDkwNzI1MDIxMjE0WhcNMDkxMTAyMDIxMjE0WjAu
-MBUCAgOrGA8yMDA5MDcyNDIxMTIxNFowFQICAQAYDzIwMDkwNzI0MjExMjE0WjAN
-BgkqhkiG9w0BAQQFAAOBgQApflU91pdbbSXNMLxRHAwz+2M2vzhmpFDYsX8gPe76
-GgrEY475v1CGJTdmKQnwosUx1tJ6HgoueAfTvzLGgVhqfeeR6BTjhnJH69rW+L6A
-w47xSB7rmUglsn3HlAdZl4tIex+SlH7AB1mEsWNJ0VA0mDEF01eOaBwBfEmK3zGd
-ng==
+D1Rlc3RpbmcgUm9vdCBDQRcNMDkwNzI2MDQzNDU2WhcNMTIwOTI3MDI0MTUyWjA8
+MBUCAgOrGA8yMDA5MDcyNTIzMzQ1NlowIwICAQAYDzIwMDkwNzI1MjMzNDU2WjAM
+MAoGA1UdFQQDCgEEMA0GCSqGSIb3DQEBBAUAA4GBAEBt7xTs2htdD3d4ErrcGAw1
+4dKcVnIWTutoI7xxen26Wwvh8VCsT7i/UeP+rBl9rC/kfjWjzQk3/zleaarGTpBT
+0yp4HXRFFoRhhSE/hP+eteaPXRgrsNRLHe9ZDd69wmh7J1wMDb0m81RG7kqcbsid
+vrzEeLDRiiPl92dyyWmu
-----END X509 CRL-----
"""
crl = load_crl(FILETYPE_PEM, crl_txt)
revs = crl.get_revoked()
self.assertEqual(len(revs), 2)
self.assertEqual(revs[0].get_serial(), '03AB')
+ self.assertEqual(revs[0].get_reason(), None)
self.assertEqual(revs[1].get_serial(), '0100')
+ self.assertEqual(revs[1].get_reason(), 'Superseded')
der = _runopenssl(crl_txt, "crl", "-outform", "DER")
crl = load_crl(FILETYPE_ASN1, der)
revs = crl.get_revoked()
self.assertEqual(len(revs), 2)
self.assertEqual(revs[0].get_serial(), '03AB')
+ self.assertEqual(revs[0].get_reason(), None)
self.assertEqual(revs[1].get_serial(), '0100')
-
+ self.assertEqual(revs[1].get_reason(), 'Superseded')