Add FILETYPE_TEXT for dumping keys and certificates to a text format
diff --git a/ChangeLog b/ChangeLog
index 75a9745..6e70a4d 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,8 @@
+2009-04-01  Jean-Paul Calderone  <exarkun@twistedmatrix.com>
+
+	* src/crypto/crypto.c: Add FILETYPE_TEXT for dumping keys and
+	  certificates and certificate signature requests to a text format.
+
 2008-12-31  Jean-Paul Calderone  <exarkun@twistedmatrix.com>
 
 	* src/crypto/x509ext.c, test/test_crypto.py: Add the get_short_name
diff --git a/src/crypto/crypto.c b/src/crypto/crypto.c
index a2b62c4..257c016 100644
--- a/src/crypto/crypto.c
+++ b/src/crypto/crypto.c
@@ -147,6 +147,7 @@
     pem_password_cb *cb = NULL;
     void *cb_arg = NULL;
     BIO *bio;
+    RSA *rsa;
     crypto_PKeyObj *pkey;
 
     if (!PyArg_ParseTuple(args, "iO!|sO:dump_privatekey", &type,
@@ -199,8 +200,14 @@
             ret = i2d_PrivateKey_bio(bio, pkey->pkey);
             break;
 
+        case X509_FILETYPE_TEXT:
+            rsa = EVP_PKEY_get1_RSA(pkey->pkey);
+            ret = RSA_print(bio, rsa, 0);
+            RSA_free(rsa); 
+            break;
+
         default:
-            PyErr_SetString(PyExc_ValueError, "type argument must be FILETYPE_PEM or FILETYPE_ASN1");
+            PyErr_SetString(PyExc_ValueError, "type argument must be FILETYPE_PEM, FILETYPE_ASN1, or FILETYPE_TEXT");
             BIO_free(bio);
             return NULL;
     }
@@ -302,8 +309,12 @@
             ret = i2d_X509_bio(bio, cert->x509);
             break;
 
+        case X509_FILETYPE_TEXT:
+            ret = X509_print_ex(bio, cert->x509, 0, 0);
+            break;
+
         default:
-            PyErr_SetString(PyExc_ValueError, "type argument must be FILETYPE_PEM or FILETYPE_ASN1");
+            PyErr_SetString(PyExc_ValueError, "type argument must be FILETYPE_PEM, FILETYPE_ASN1, or FILETYPE_TEXT");
             BIO_free(bio);
             return NULL;
     }
@@ -405,8 +416,12 @@
             ret = i2d_X509_REQ_bio(bio, req->x509_req);
             break;
 
+        case X509_FILETYPE_TEXT:
+            ret = X509_REQ_print_ex(bio, req->x509_req, 0, 0);
+            break;
+
         default:
-            PyErr_SetString(PyExc_ValueError, "type argument must be FILETYPE_PEM or FILETYPE_ASN1");
+            PyErr_SetString(PyExc_ValueError, "type argument must be FILETYPE_PEM, FILETYPE_ASN1, or FILETYPE_TEXT");
             BIO_free(bio);
             return NULL;
     }
@@ -801,6 +816,7 @@
 
     PyModule_AddIntConstant(module, "FILETYPE_PEM",  X509_FILETYPE_PEM);
     PyModule_AddIntConstant(module, "FILETYPE_ASN1", X509_FILETYPE_ASN1);
+    PyModule_AddIntConstant(module, "FILETYPE_TEXT", X509_FILETYPE_TEXT);
 
     PyModule_AddIntConstant(module, "TYPE_RSA", crypto_TYPE_RSA);
     PyModule_AddIntConstant(module, "TYPE_DSA", crypto_TYPE_DSA);
diff --git a/src/crypto/crypto.h b/src/crypto/crypto.h
index 0a71d2a..2322720 100644
--- a/src/crypto/crypto.h
+++ b/src/crypto/crypto.h
@@ -117,4 +117,12 @@
 
 #endif /* crypto_MODULE */
 
+/* Define a new type for emitting text.  Hopefully these don't collide with
+ * future official OpenSSL constants, but the switch statement of
+ * dump_certificate() will alert us if it matters.
+ */
+#ifndef X509_FILETYPE_TEXT 
+#define X509_FILETYPE_TEXT  (58)
+#endif
+
 #endif /* PyOpenSSL_CRYPTO_H_ */
diff --git a/test/test_crypto.py b/test/test_crypto.py
index d724abf..214a4b8 100644
--- a/test/test_crypto.py
+++ b/test/test_crypto.py
@@ -4,14 +4,17 @@
 Unit tests for L{OpenSSL.crypto}.
 """
 
-from unittest import TestCase
+from unittest import TestCase, main
+from os import popen2
 
 from OpenSSL.crypto import TYPE_RSA, TYPE_DSA, Error, PKey, PKeyType
 from OpenSSL.crypto import X509, X509Type, X509Name, X509NameType
 from OpenSSL.crypto import X509Req, X509ReqType
 from OpenSSL.crypto import X509Extension, X509ExtensionType
-from OpenSSL.crypto import FILETYPE_PEM, load_certificate, load_privatekey
-from OpenSSL.crypto import dump_privatekey
+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 dump_certificate_request, dump_privatekey
 
 
 cleartextCertificatePEM = """-----BEGIN CERTIFICATE-----
@@ -51,6 +54,19 @@
 -----END RSA PRIVATE KEY-----
 """
 
+cleartextCertificateRequestPEM = (
+    "-----BEGIN CERTIFICATE REQUEST-----\n"
+    "MIIBnjCCAQcCAQAwXjELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAklMMRAwDgYDVQQH\n"
+    "EwdDaGljYWdvMRcwFQYDVQQKEw5NeSBDb21wYW55IEx0ZDEXMBUGA1UEAxMORnJl\n"
+    "ZGVyaWNrIERlYW4wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBANp6Y17WzKSw\n"
+    "BsUWkXdqg6tnXy8H8hA1msCMWpc+/2KJ4mbv5NyD6UD+/SqagQqulPbF/DFea9nA\n"
+    "E0zhmHJELcM8gUTIlXv/cgDWnmK4xj8YkjVUiCdqKRAKeuzLG1pGmwwF5lGeJpXN\n"
+    "xQn5ecR0UYSOWj6TTGXB9VyUMQzCClcBAgMBAAGgADANBgkqhkiG9w0BAQUFAAOB\n"
+    "gQAAJGuF/R/GGbeC7FbFW+aJgr9ee0Xbl6nlhu7pTe67k+iiKT2dsl2ti68MVTnu\n"
+    "Vrb3HUNqOkiwsJf6kCtq5oPn3QVYzTa76Dt2y3Rtzv6boRSlmlfrgS92GNma8JfR\n"
+    "oICQk3nAudi6zl1Dix3BCv1pUp5KMtGn3MeDEi6QFGy2rA==\n"
+    "-----END CERTIFICATE REQUEST-----\n")
+
 encryptedPrivateKeyPEM = """-----BEGIN RSA PRIVATE KEY-----
 Proc-Type: 4,ENCRYPTED
 DEK-Info: DES-EDE3-CBC,9573604A18579E9E
@@ -711,6 +727,73 @@
         self.assertEqual(loadedKey.bits(), key.bits())
 
 
+    def _runopenssl(self, pem, *args):
+        """
+        Run the command line openssl tool with the given arguments and write
+        the given PEM to its stdin.
+        """
+        write, read = popen2(" ".join(("openssl",) + args))
+        write.write(pem)
+        write.close()
+        return read.read()
+
+
+    def test_dump_certificate(self):
+        """
+        L{dump_certificate} writes PEM, DER, and text.
+        """
+        pemData = cleartextCertificatePEM + cleartextPrivateKeyPEM
+        cert = load_certificate(FILETYPE_PEM, pemData)
+        dumped_pem = dump_certificate(FILETYPE_PEM, cert)
+        self.assertEqual(dumped_pem, cleartextCertificatePEM)
+        dumped_der = dump_certificate(FILETYPE_ASN1, cert)
+        good_der = self._runopenssl(dumped_pem, "x509", "-outform", "DER")
+        self.assertEqual(dumped_der, good_der)
+        cert2 = load_certificate(FILETYPE_ASN1, dumped_der)
+        dumped_pem2 = dump_certificate(FILETYPE_PEM, cert2)
+        self.assertEqual(dumped_pem2, cleartextCertificatePEM)
+        dumped_text = dump_certificate(FILETYPE_TEXT, cert)
+        good_text = self._runopenssl(dumped_pem, "x509", "-noout", "-text")
+        self.assertEqual(dumped_text, good_text)
+
+
+    def test_dump_privatekey(self):
+        """
+        L{dump_privatekey} writes a PEM, DER, and text.
+        """
+        key = load_privatekey(FILETYPE_PEM, cleartextPrivateKeyPEM)
+        dumped_pem = dump_privatekey(FILETYPE_PEM, key)
+        self.assertEqual(dumped_pem, cleartextPrivateKeyPEM)
+        dumped_der = dump_privatekey(FILETYPE_ASN1, key)
+        # XXX This OpenSSL call writes "writing RSA key" to standard out.  Sad.
+        good_der = self._runopenssl(dumped_pem, "rsa", "-outform", "DER")
+        self.assertEqual(dumped_der, good_der)
+        key2 = load_privatekey(FILETYPE_ASN1, dumped_der)
+        dumped_pem2 = dump_privatekey(FILETYPE_PEM, key2)
+        self.assertEqual(dumped_pem2, cleartextPrivateKeyPEM)
+        dumped_text = dump_privatekey(FILETYPE_TEXT, key)
+        good_text = self._runopenssl(dumped_pem, "rsa", "-noout", "-text")
+        self.assertEqual(dumped_text, good_text)
+
+
+    def test_dump_certificate_request(self):
+        """
+        L{dump_certificate_request} writes a PEM, DER, and text.
+        """
+        req = load_certificate_request(FILETYPE_PEM, cleartextCertificateRequestPEM)
+        dumped_pem = dump_certificate_request(FILETYPE_PEM, req)
+        self.assertEqual(dumped_pem, cleartextCertificateRequestPEM)
+        dumped_der = dump_certificate_request(FILETYPE_ASN1, req)
+        good_der = self._runopenssl(dumped_pem, "req", "-outform", "DER")
+        self.assertEqual(dumped_der, good_der)
+        req2 = load_certificate_request(FILETYPE_ASN1, dumped_der)
+        dumped_pem2 = dump_certificate_request(FILETYPE_PEM, req2)
+        self.assertEqual(dumped_pem2, cleartextCertificateRequestPEM)
+        dumped_text = dump_certificate_request(FILETYPE_TEXT, req)
+        good_text = self._runopenssl(dumped_pem, "req", "-noout", "-text")
+        self.assertEqual(dumped_text, good_text)
+
+
     def test_dump_privatekey_passphraseCallback(self):
         """
         L{dump_privatekey} writes an encrypted PEM when given a callback which
@@ -729,3 +812,7 @@
         self.assertTrue(isinstance(loadedKey, PKeyType))
         self.assertEqual(loadedKey.type(), key.type())
         self.assertEqual(loadedKey.bits(), key.bits())
+
+
+if __name__ == '__main__':
+    main()
diff --git a/test/test_ssl.py b/test/test_ssl.py
index 729947c..32d8f74 100644
--- a/test/test_ssl.py
+++ b/test/test_ssl.py
@@ -15,7 +15,7 @@
     from twisted.trial.unittest import TestCase
 except ImportError:
     # Fall back to the stdlib TestCase though, since it kind of works.
-    from unittest import TestCase
+    from unittest import TestCase, main
 
 from OpenSSL.crypto import TYPE_RSA, FILETYPE_PEM, PKey, dump_privatekey, load_certificate, load_privatekey
 from OpenSSL.SSL import WantReadError, Context, Connection, Error
@@ -311,3 +311,8 @@
             self.assertEqual(OP_NO_TICKET, 0x4000)
     else:
         "OP_NO_TICKET unavailable - OpenSSL version may be too old"
+
+
+if __name__ == '__main__':
+    main()
+