diff options
author | Anas Nashif <anas.nashif@intel.com> | 2013-02-11 21:10:32 -0800 |
---|---|---|
committer | Anas Nashif <anas.nashif@intel.com> | 2013-02-11 21:10:32 -0800 |
commit | 1112fa2720ec58bb98a2ae14d9353d7a43d9d757 (patch) | |
tree | 961b43fa3f434a386be81e26a4191c8f91a30c1b | |
download | python-M2Crypto-1112fa2720ec58bb98a2ae14d9353d7a43d9d757.tar.gz python-M2Crypto-1112fa2720ec58bb98a2ae14d9353d7a43d9d757.tar.bz2 python-M2Crypto-1112fa2720ec58bb98a2ae14d9353d7a43d9d757.zip |
Imported Upstream version 0.21.1
355 files changed, 44868 insertions, 0 deletions
@@ -0,0 +1,488 @@ +0.21.1 - 2011-01-15 +------------------- +- Distribution fix + +0.21 - 2011-01-12 +----------------- +- Support OpenSSL 1.0. Thanks to Miloslav Trmac for figuring out how to fix + test_smime.py +- Rename m2.engine_init to engine_init_error so that + ENGINE_init and ENGINE_finish can be exposed, thanks to Erlo +- 0.20 started releasing Python locks even around some operations that + interacted with the Python runtime, potentially causing crashes and other + weirdness, fix by Miloslav Trmac +- Make httpslib.ProxyHTTPSConnection work with Python 2.3 + +0.20.2 - 2009-10-06 +------------------- +- (Re)Enable configuration and use with OpenSSL 0.9.7g and older by disabling + RSA PSS methods when using such old OpenSSL, thanks to Stef Walter + +0.20.1 - 2009-08-27 +------------------- +- Fix regression in httpslib.ProxyHTTPSConnection, by Miloslav Trmac + +0.20 - 2009-08-10 +----------------- +- Deprecated M2Crypto.PGP subpackage since nobody seems to be using it nor + is it being maintained (if you do use it, please let me know) +- Added fedora_setup.sh to help work around differences on Fedora Core -based + distributions (RedHat, CentOS, ...); thanks to Miloslav Trmac +- Added X509.load_request_bio and load_request_string, by Hartmut Goebel and + Pavel Shramov +- Added alias X509.Request.set_subject for set_subject_name to match X509.X509, + by Pavel Shramov +- OBJ_* wrappers did not work properly with OpenSSL 0.9.8a and earlier, fix by + Pavel Shramov +- Added ASN1_UTCTIME.get_datetime and set_datetime, by Pavel Shramov +- Fixed obj_obj2txt, which returned nonsense, fix by Barney Stratford +- m2urllib did not close sockets properly, fix by Miloslav Trmac +- Allow SSL peer certificate to have subjectAltName without dNSName and use + commonName for hostname check, fix by Miloslav Trmac +- threading_locking_callback did not block on a lock when the lock + was held by another thread, by Miloslav Trmac +- Allow more blocking OpenSSL functions to run without GIL, by Miloslav Trmac +- Fixed httpslib to send only the path+query+fragment part of the URL when + using CONNECT proxy, by James Bowes +- SSLServer.__init__ now takes optional bind_and_activate parameter and + initializes by calling SocketServer.BaseServer.__init__, which + are Python 2.6 compatibility fixes, by Christian +- ftpslib now works with Python 2.6, by Theodore A. Roth +- httpslib.ProxyHTTPSConnection needs to cast port into integer, + by John M. Schanck +- Added support for RSASSA-PSS signing and verifying, by Chris Collis +- Added support for disabling padding when using RSA encryption, + by Chris Collis +- ASN1_INTEGERs can now be larger than fits in an int, for example to support + X509 certificates with large serial numbers, + patch by Mikhail Vorozhtsov and testcase by Barry G. +- Reverted a change done in 0.17 to m2urllib2 which changed urls to include + host when it should stay as it was +- httpslib no longer uses urllib; instead it uses urlparse for url parsing +- SMIME.text_crlf and text_crlf_bio were always raising TypeError; fixed +- EVP.load_key and load_key_bio fixed to raise EVP.EVPError and BIO.BIOError + instead of str (str exceptions not allowed in Python 2.6 and later) +- SSL.Session.load_session fixed to raise SSL.SSLError instead of str +- SMIME.load_pkcs7, load_pkcs7_bio, smime_load_pkcs7, smime_load_pkcs7_bio, + text_crlf, text_crlf_bio fixed to raise BIO.BIOError, SMIME.PKCS7_Error and + SMIME.SMIME_Error as appropriate instead of str +- Added FIPS mode to unit tests, and used FIPS-compliant key sizes in other + tests, by Miloslav Trmac. Note that tests run much slower because of this! +- Unit tests cover 80% of the code + +0.19.1 - 2008-10-12 +------------------- +- Re-enable building when OpenSSL built without EC support, by Miloslav Trmac +- Remove shebang from Engine.py since it is not executable, by Miloslav Trmac + +0.19 - 2008-10-05 +----------------- +- OpenSSL OBJ_* functions wrapped by Pavel Shramov +- OpenSSL ENGINE interface wrapped, providing support for smart cards, by + Martin Paljak and Pavel Shramov +- EVP.PKey.get_rsa() now returns RSA_pub, which fixes segmentation fault + when trying to encrypt using public key from X509 certificate, by Ben Timby +- httpslib.ProxyHTTPSConnection now sends the required Host header, + by Karl Grindley +- Use the proxied User-Agent value in CONNECT requests, by James Antill and + Miloslav Trmac +- Fixed m2urllib.build_opener when optional handlers were in use, + affected Python 2.5 and later, by Miloslav Trmac +- Reverted the incorrect GIL change done in 0.18 to m2.passphrase_callback, + which caused a deadlock when called from mod_python for example. Thanks to + Michal Kochel and Keith Jackson. +- SSL.Connection.accept() passed wrong certificate to postConnectionCheck + callback +- httpslib.HTTPSConnection now raises ValueError for illegal keyword argument +- m2.pkey_write_pem[_no_cipher] changed to use the recommended (more secure) + PEM_write_bio_PKCS8PrivateKey (used by PEM_write_bio_PrivateKey). +- X509.load_cert, load_cert_bio, load_cert_der_string, new_stack_from_der, + load_request and load_crl now raise X509Error for invalid data. Previously + some of these raised a string as an error, some did not raise but caused + strange errors later, for example x509.verify() would return -1. +- Fixed SSL.Connection.get_socket_read_timeout and set_socket_read_timeout on + 64bit platforms by adding SSL.timeout.struct_size() and using it instead of + hardcoded size for socket.getsockopt +- X509_Store.load_info now returns the value from the underlying + m2.x509_store_load_locations call, and in case of error raises X509Error +- Fixed SMIME.verify to raise the correct PKCS7_Error (used to raise + SMIME_Error) when verification fails with Python 2.6 + +0.18.2 - 2007-10-12 +------------------- +- typedef Py_ssize_t was insufficiently guarded, now follows PEP 353. This + prevented building on at least Red Hat Linux and Debian Linux (unstable). + +0.18.1 - 2007-10-08 +------------------- +- Redo build fix when OpenSSL configured without Elliptic Curves (EC), see + also INSTALL file + +0.18 - 2007-07-26 +----------------- +- Added EVP.pbkdf2 to derive key from password +- X509_Store_Context.get1_chain added +- Added X509_Name.__iter__, __getitem__, get_entries_by_nid which allow + iterating over all X509_Name_Entries or getting just all commonName entries, + for example +- Added X509_Name_Entry.get_object, get_data, set_data +- Added back PKCS7.get0_signers (was removed in 0.16) +- X509_Extension.get_value accepts flag and indent parameters. +- support multiple dNSName fields in subjectAltName +- support multiple commonName fields for SSL peer hostname checking +- Checking for erroneous returns from more OpenSSL EVP_* functions, which + means that certain things that used to fail silently will now raise an + EVP.EVPError; affected m2 functions are: digest_final, cipher_init, + cipher_update, cipher_final and sign_update. sign_final will now raise + EVP.EVPError instead of SystemError as well. +- Fixed Pkey.verify_final to take a sign parameter +- If a subjectAltName extension of type dNSName is present in peer certificate, + use only the dNSNames when checking peer certificate hostname, as specified + by RFC 2818. If no dNSNames are present, use subject commonName. +- Fixed memory leaks in m2 functions ec_key_new_by_curve_name, + pkey_get_modulus, ecdsa_verify, threading_init and + X509.X509.verify, X509.X509_Stack (which manifested for example when + calling X509.new_stack_from_der), SSL.Connection (which manifested with some + connection errors or when connect was never called), twisted wrapper, + SSL.Connection.makefile (in BIO.IOBuffer really) +- Fixed threading regressions introduced in 0.16, + by Aaron Reizes and Keith Jackson +- Added SSL session caching support to HTTPSConnection, by Keith Jackson +- Added the ability to save and load DER formatted X509 certificates and + certificate requests, by Keith Jackson +- m2xmlrpclib.py fixed to work with Python 2.5, by Miloslav Trmac +- 64-bit correctness fixes, by Miloslav Trmac +- Added X509_Name.as_hash, by Thomas Uram +- Moved --openssl option from general setup.py option to build_ext option, + meaning you need to do: python setup.py build build_ext --openssl=/path, + by Philip Kershaw +- Fixed build problem affecting certain systems where OpenSSL was built without + EC support +- M2CRYPTO_TEST_SSL_SLEEP environment variable controls how long to sleep + after starting the test SSL server. Default is 0.5, but 0.1 or even 0.05 + might work with modern computers. Makes tests finish significantly faster. + +0.17 - 2006-12-20 +----------------- +- setup.py has new test command to run unit tests (requires setuptools) +- Added m2urllib2, by James Bowes (python 2.4 and later, at least for now) +- Added CONNECT proxy for httpslib and m2urllib2, by James Bowes +- Added PKey.get_modulus, X509.get_fingerprint, X509_Name.as_der and + m2.bn_to_hex, by Thomas Uram +- Prevent Connection.makefile from freeing bio redundantly, by Thomas Uram +- Added Err.peek_error_code, by Thomas Uram +- Fixed m2urllib.open_https to return the response headers, otherwise code + that relied on that would break (for example msnlib-3.5), by Arno bakker +- Fixed twisted wrapper to work with >16kb BIO buffers, by Martin Paljak +- Added support for remaining ECs, by Larry Bugbee +- Fixed DSA.save_key and DSA_.save_pub_key, by Larry Bugbee +- SSL.Context.load_verify_locations raises ValueError if cafile and capath + are both None +- Fixed X509.check_purpose() (was always raising exceptions) +- smime_read_pkcs7 was changed to automatically call BIO_set_mem_eof_return + on memory BIOs because otherwise the read would fail with + "SMIME_Error: not enough data" +- X509.new_extension('subjectKeyIdentifier', 'hash') raises ValueError instead + of crashing Python + +0.16 - 2006-07-05 +----------------- +- Minimum requirements updated: Python 2.3+, OpenSSL 0.9.7+, SWIG 1.3.24+ +- Optional features from OpenSSL 0.9.8 and newer +- Enhancements to EVP and X509 to allow proxy certificate handling, + by Matt Rodriguez +- SSLBio and related additions to help do SSL with BIOs directly, + by Matt Rodriguez +- Added --openssl option to build command which can be used to specify + where OpenSSL is installed, by Matt Rodriguez +- Added sign and verify to RSA class, and get_rsa to PKey class, + by Matt Rodriguez +- ECDSA signatures and ECDH key agreement, requires OpenSSL 0.9.8+, + by Arno Bakker +- Fix non-hashable type problems in SSL._ctxmap and users, + by Michael Weiser +- Fixed SSLServer.handle_error to take the correct number of + arguments, by Dan Williams +- Various DSA enhancements by Larry Bugbee +- Added sha224, sha256, sha384 and sha512, by Larry Bugbee +- Added serialNumber, SN, surname, GN and givenName fields to X509_Name, + by Martin Paljak +- m2.X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT is the fourth certificate + verification error that will be allowed when unknown CAs are allowed +- post connection checks in Connection.accept() and connect() fixed (these + were broken in 0.15) +- Fixed EVP.Cipher to work with aes_* ciphers (used to crash Python). + The actual problem was in m2.bytes_to_key. +- SMIME methods and functions raise correct exceptions +- Raise ValueError instead of AttributeError when a non-existing hash + algorithm or SSL version is asked for +- ssl_ctx_set_tmp_(dh|rsa) now return value, and the rsa version calls + the rsa function instead of the dh function +- digest_update and verify_update return type changed to int, which allows + better error reporting; EVP.MessageDigest.update and + EVP.PKey.verify_update likewise changed +- X509_Name and ASN1_String as_text (new for ASN1_String) take optional + parameters to control formatting. +- Cipher_Stack, X509_Stack and X509_Extension_Stack are iterable +- EVP.MessageDigest now properly cleans up the underlying data when the object + gets deleted +- It is now possible to set and get non-nid values to X509_Name (previously + only set worked) +- SSL.Connection.set_client_CA_list_from_file now uses the actual implementd + function instead of raising exception +- Multithreaded SSL no longer uses the SSL_set/get_app_data to set and + restore thread state, but uses the standard PyGILState_STATE instead. +- m2urllib no longer outputs the HTTP headers (there was an erronous call + to set_debuglevel(1)) +- Removed RCS_id, RCS_ID and _RCS_id from Python files +- All known memory leaks fixed +- SWIG and compiler warning fixes +- More and better Epydoc formatted docstrings +- More than doubled the number of unit tests, also made many demos into tests + +0.15 - 2005-08-17 +----------------- +- Support OpenSSL 0.9.8, Python 2.4.1, SWIG 1.3.24 +- Fixed multiple memory leaks +- Twisted integration +- Safer defaults for SSL context and post connection check for clients +- Eliminated C pointers from interfaces (some may still remain in callbacks) +- Many cases where Python interpreter crashed have been fixed +- Improved thread safety of many callbacks +- And of course more of the OpenSSL API is covered, new docstrings and + tests have been written + + Changes since 0.13 +-------------------- +- Fixed memory leak due to circular reference in SSL.Connection. + Thanks to Michael Dunstan. Oops, patch is ZServerSSL-specific. + Andre Reitz provided a generalised fix. Thanks Andre. +- Fixed __getattr__ error in DSA. Thanks to Igor Belyi. +- Added rand_poll, rand_screen and rand_win32_event functions to + M2Crypto.Rand. +- Updated ZServerSSL files to match Zope 2.7.0 versions. +- Integrated (overlapping) patches by Peter Teniz and Heikki Toivonen + covering operations on X.509-related structures that gives M2Crypto + PKI functionality. Thanks Peter and Heikki. +- Peter Teniz contributed demo2004/pki/x509auth.py. +- Created demo2004/ directory that will contain new or updated demos. +- Added verify_[init|update|final] in _evp.i. Patch by Zachery Corbiere. + Thanks Zac. + + + Changes since 0.12/0.11 +------------------------- +- Patches from Artur Frysiak <wiget@pld-linux.org>. Thanks Artur. + = Allow using a passphrase callback in class SMIME. + = Added method get0_signers to class PKCS7, which retrieves signers' + certificates from a PKCS7 blob. + = Added methods as_pem and save_pem to class X509. + = Added file version.py. + = Allow SSL.Context.load_verify_locations to accept both 'cafile' and + 'capath'. +- Fixed BIO.read() not reading until EOF. Thanks to Egil Muller + <redhog@redhog.org> for suggestion. +- Honour 'mode' parameter in SSL.Connection.makefile. Thanks again to Egil + Muller. +- Roger Binns contributed epydoc-generated docs for M2Crypto. Thanks Roger. +- Peter Teniz contributed patches to create X.509 requests and certificates. + Thanks Peter. +- Updated Medusa to 0.54. +- Make various OpenSSL bignum functions (written long ago) available to Python. + + + Changes since 0.11 +-------------------- +- ZServerSSL with client certificate-based authentication rides again. +- Created Makefile for Python 2.3. +- Modified LICENCE: changed my name to the generic "the author" in the + all-caps disclaimer paragraph. +- Allow to save RSA key pair in the clear. +- ZServerSSL for Zope 2.7. +- Excluded RC5. IDEA was taken out several releases ago. This should + allow M2Crypto to build with stock OpenSSL on various Linuxen. +- Added ssl_set_tmp_dh_callback. +- Added ssl_set_tmp_rsa and ssl_set_tmp_rsa_callback to support weak-cipher + browsers. +- ZServerSSL exports SSL_CIPHER request header (a la mod_ssl) to Zope + applications. +- Perform distutils's SWIG .i search path tweaking within setup.py. setup.py + should now work "out of the box". +- Added contrib/smimeplus.py, a high-level S/MIME interface, contributed by + Bernard Yue <bernie@3captus.com>. Thanks Bernard. +- Added in long forms of nid's in X509_Name. Thanks to William K Volkman + <development@netshark.com> for patch. +- Updated Mac OS X build instructions. Thanks to Larry Bugbee + + + Changes since 0.10 +-------------------- +- Dave Berkeley <dave@rotwang.freeserve.co.uk> contributed fixes to + SSL.Context-related memory leaks and code to set the size of the SSL + session cache. +- Brent Chun <bnc@intel-research.net> contributed the following: + + Fixes to memory leaks. + + Code to expose X.509 certificate chain operations. + + Code to expose set/get operations on the SSL session cache. +- Changed swig/ to SWIG/, for the convenience of people who don't read + INSTALL. Some Makefiles may break because of this. setup.py continues + to work, of course. +- ZServerSSL tested with Zope 2.6.1. There is now a HOWTO. +- Updated README and INSTALL. +- Filled doc/ with stuff that went missing in several past releases. + + + Changes since 0.09 +-------------------- +- Updated to OpenSSL 0.9.7. Thanks to Toby Allsopp <toby@MI6.GEN.NZ> for + patches. +- Added functionality to create a basic certificate request. Also + contributed by Toby Allsopp. +- Finally, AES! + + + Changes since 0.08 +-------------------- +- Replaced demo/Zope/ZServer/__init__.py with the correct version + for Zope 2.6.0. +- Added a sample starts.bat for ZServerSSL. +- Incoporated a patch by prashanth@jibe.biz that handled the + new-in-Python-2.2.2 "strict" parameter for the various HTTP[S] connection + classes in httplib.py. Thanks prashanth. This fixes M2Crypto's XMLRPC + support for Python 2.2.2. (Apparently it was working for Python 2.2.1.) +- Incorporated some cosmetic patches from Adam Karpierz <karpierz@zope.pl>. + Thanks Adam. + + + Changes since 0.07 snapshot #3 +-------------------------------- +- Updated to SWIG 1.3.17. +- Excluded IDEA. +- Tested with OpenSSL 0.9.6h. +- ZServerSSL rides again for Zope 2.6.0. +- setup.py does! +- Removed Makefiles for Windows and Unix. (Makefile.osx remains.) +- Included in contrib/ Isaac Salzberg's application of Mihai Ibanescu's + patch that allows IIS interoperability thru an authenticating proxy. + Thanks Isaac. +- Included in contrib/ patch by Dave Brueck <dave@pythonaprocrypha.com> + that has smarter non-blocking behaviour. Thanks Dave. + + + Changes since 0.06 +----------------------- +- test_ssl_win.py. (Requires Mark Hammond's Win32 extensions.) +- Renamed demo/https to demo/medusa; updated Medusa to 2001 Jun release. +- Improved _ssl.i's and M2Crypto.SSL.Connection's accept/connect methods. +- M2Crypto.ftpslib for client-side FTP/TLS. +- demo/medusa/ftps_server.py for server-side FTP/TLS. +- Improved thread-safety. +- Cleaned up echo client and servers. +- Fixed missing import in m2urllib. +- Fixed m2urllib to handle HTTP redirects. +- Python 2.2 compatibility. +- AuthCookie - secure authenticator cookies. + + + Changes since 0.05 +----------------------- +- Handled the cases where Python callbacks raised exceptions. +- Fixed a NULL-deref bug in _ssl.i which crashes Medusa https when IE + or Opera comes a-calling. +- ZServerSSL rides again - a more robust ZServerSSL for Zope 2.3.0. +- Added the MIME type 'application/x-x509-ca-cert' to + demo/ssl/https_srv.py. This facilitates installing self-generated + certificates into your browser. +- ZSmime and GuardedFile bundled. +- Documentation! A HOWTO on operating your own CA. +- Documentation! A HOWTO on S/MIME. Examples are in demo/smime.howto. +- Python 2.1 compatibility. +- Fixed demo/https/https_server.py's CPU-spinning. (As per ZServerSSL.) +- Fixed m2urllib's unexpected eof - demo/ssl/urllib_cli.py now works. +- Renamed xmlrpclib2.py to m2xmlrpclib.py. +- Kludged SSL.ssl_dispatcher to do blocking connect()'s: see + demo/ssl/https_cli_async.py. +- SWIG 1.3.6 does! Thanks to Keith Jackson <krjackson@lbl.gov>. + + + Changes since 0.04 +----------------------- +- Fixed a silly reversed-logic bug in M2Crypto.SSL.Connection.setblocking(). +- Fixed yet more memory leaks. Thanks to Ray Suorsa <res@loudcloud.com>. +- Build instructions for Borland BC++ 5.5 free compiler suite. +- Bundles the June 2000 unencumbered release of Medusa. +- SSL callback thread-safety. Thanks again to Ray Suorsa for insights and + patches. +- Renamed M2Crypto.M2Crypto to M2Crypto.m2 to prevent package/module loading + confusion. +- SSL.Session and a demo in demo/ssl/sess.py. +- https_srv.py, an enhanced, https version of SimpleHTTPServer.py. +- Interface change: SMIME.load_pkcs7_bio() is renamed + SMIME.smime_load_pkcs7_bio(), similarly SMIME.load_pkcs7() to + SMIME.smime_load_pkcs7(); these load PKCS7 objects generated by S/MIME. +- Interface change: SMIME.load_pkcs7_bio() now loads a PKCS7 PEM file, i.e., a + file of the format "-----BEGIN PKCS7-----". +- Works with both Python 2.0 and Python 1.5.2. +- OpenSSL 0.9.6. (Possibly incompatible with earlier OpenSSL releases.) +- Unit tests with PyUnit. +- Improved C code: + = Custom Python exceptions. + = Diligent error checking. + = Fixed memory leaks. +- Renamed M2Crypto.urllib2 to M2Crypto.m2urllib. +- HTTPS clients of Python 1.5.2's and Python 2.0's httplib and urllib. + + + Changes since 0.03 +----------------------- +- SSL certificate-based authentication with Python callback. +- More robust SSL.Connection - raises exceptions, not dumps core. +- Fixed (some) memory leaks and multiple-free()s. +- Cleaned up EVP.HMAC and EVP.PKey. +- More X.509 certificate manipulation. +- An interface to create SSL sessions. +- Unified SSL read() and write() for synchronous and asynchronous operation. +- S/MIME and PKCS #7. +- Integrated with OpenSSL 0.9.5. +- Enhanced the PRNG interface. + + + Changes since 0.02 +----------------------- +1. Ephemeral DH for SSL. +2. ThreadingSSLServer now does. +3. XMLrpc over https. +4. ZServerSSL for Zope 2.1.3. +5. Encrypting monitor for Zope 2.1.3. +6. Beginnings of PGP2 support. +7. Replaced eval() calls with other (hopefully) safe ones. +8. Miscellaneous enhancements and bug fixes. + + + Changes since 0.01 +----------------------- +1. Beginnings of SSL support. + + For building servers, blocking i/o: + - An SSLServer modeled after SocketServer. + - A ForkingSSLServer that seems to work well. + - A ThreadingSSLServer that runs one thread at a time. (!) ;-) + + For building servers, nonblocking i/o: + - An ssl_dispatcher modeled after asyncore.dispatcher. + + A HTTPS server based on Medusa. + + For client-side web programming: + - httpslib + - urllib2 + + +2. Support for some BIO objects. +3. Reduced per-module name space pollution. +4. Have Swig check for NULL pointers: reduced .i cut-&-paste. +5. Standardise on MPINT for passing big integers between Python and OpenSSL. +6. Removed MD5, SHA1, RIPEMD160. Just use EVP.MessageDigest. +7. Removed HMAC. Just use EVP.HMAC. + + @@ -0,0 +1,226 @@ +==================== + Installing M2Crypto +==================== + +:Maintainer: Heikki Toivonen +:Web-Site: http://chandlerproject.org/Projects/MeTooCrypto + +.. contents:: + + +Pre-requisites +---------------- + +The following software packages are pre-requisites: + +- **Python 2.3 or newer** +- **OpenSSL 0.9.7 or newer** +- **SWIG 1.3.28 or newer** + +Note about OpenSSL versions early in the 0.9.7 series +----------------------------------------------------- + +Early OpenSSL 0.9.7 versions require the __i386__ symbol to be defined. +Uncomment this line in setup.py: + + #'-D__i386__', # Uncomment for early OpenSSL 0.9.7 versions + +if you get this compile-time error: + + This openssl-devel package does not work your architecture? + +Note about Fedora Core -based Distributions +----------------------------------------------------- + +Fedora Core (and RedHat, CentOS etc.) have made changes to OpenSSL +configuration compared to many other Linux distributions. If you can not +build M2Crypto normally, try the fedora_setup.sh script included with +M2Crypto sources. + +Installing on Unix-like systems, including Cygwin +------------------------------------------------- + +:: + + $ tar zxf m2crypto-<version>.tar.gz + $ cd m2crypto-<version> + $ python setup.py build + $ python setup.py install + +If you have installed setuptools you can also optionally run tests like this: + + $ python setup.py test + +This assumes OpenSSL is installed in /usr. You can provide an alternate +OpenSSL prefix location with --openssl option to build_ext command. Other +commands accept standard options if you need them. + +Some distributions, like Fedora Core, package OpenSSL headers in a different +location from OpenSSL itself. In that case you need to tell build_ext the +additional include location with -I option. + +Differences when installing on Windows +-------------------------------------- + +Before building from source, you need to install OpenSSL's include files, +import libraries and DLLs. By default setup.py assumes that OpenSSL include +files are in ``c:\pkg\openssl\include``, and the import libraries +in ``c:\pkg\openssl\lib``. As with other platforms, you can specify a different +OpenSSL location with --openssl option to build_ext command. + +Using OpenSSL 0.9.8 on Windows requires Python be built with applink.c +(add an include statement in python.c). This is not a requirement for +Linux or MacOSX. (applink.c is provided by OpenSSL.) + + +MSVC++ +~~~~~~~~ + +setup.py is already configured to work with MSVC++ by default. + +With MSVC++, the OpenSSL DLLs, as built, are named ``libeay32.dll`` +and ``ssleay32.dll``. Install these somewhere on your PATH; for example +in ``c:\bin``, together with ``openssl.exe``. + +For MSVC++, the import libraries, as built by OpenSSL, are named +``libeay32.lib`` and ``ssleay32.lib``. + + +MINGW +~~~~~~~ + +.. NOTE:: + The following instructions for building M2Crypto with MINGW are from + M2Crypto 0.12. These instructions should continue to work for this release, + although I have not tested them. + +Read Sebastien Sauvage's webpage: + + http://sebsauvage.net/python/mingw.html + +For mingw32, the OpenSSL import libraries are named ``libeay32.a`` and +``libssl32.a``. You may need to edit setup.py file for these. + +You'll also need to create ``libpython2[123].a``, depending on your version +of Python. + +OpenSSL DLLs for mingw32 are named ``libeay32.dll`` and ``libssl32.dll``. +Install these somewhere on your PATH; for example in +``c:\bin``, together with ``openssl.exe``. + +Build M2Crypto: + + python setup.py build -cmingw32 + python setup.py install + + +BC++ +~~~~~~ + +.. NOTE:: + The following instructions for building M2Crypto with MSVC++ 6.0 and + BC++ 5.5 free compiler suite are from M2Crypto 0.10. These instructions + should continue to work for this release, although I have not tested + them. + +For BC++ these files are created from the MSVC++-built ones using the +tool ``coff2omf.exe``. I call them ``libeay32_bc.lib`` and +``ssleay32_bc.lib``, respectively. You will need to edit setup.py file +for these. + +You'll also need Python's import library, e.g., ``python22.lib``, to +be the BC++-compatible version; i.e., create ``python22_bc.lib`` from +``python22.lib``, save a copy of ``python22.lib`` (as ``python22_vc.lib``, +say), then rename ``python22_bc.lib`` to ``python22.lib``. + + +Now you are ready to build M2Crypto. Do one of the following:: + + python setup.py build + python setup.py build -cbcpp + +Then, + +:: + + python setup.py install + + +MacOSX +------ + +Follow the standard instructions to build and install M2Crypto. +However, should you encounter difficulties, you may want to consider +the following possibilities. + + - Distutils for Python 2.5 now provides support for universal + builds (ppc and i386) and Distutils requires a recent version + of Xcode. See http://developer.apple.com/tools/download/ + + - OpenSSL 0.9.7l gets installed in /usr with Apple's Security + Update 2006-007. If you need features in OpenSSL 0.9.8, you + should consider installing 0.9.8 in /usr/local. The commands + are: + + OpenSSL: + ./config shared --prefix=/usr/local + make + make test + sudo make install [or... install_sw] + + M2Crypto: + python setup.py build build_ext --openssl=/usr/local + sudo python setup.py install build_ext --openssl=/usr/local + +To make Universal builds, you will need to uncomment a line in setup.py: + + extra_link_args = ['-Wl,-search_paths_first'], + +If that does not work, here is what Marc Hedlund was able to get working: + + First, download OpenSSL 0.9.8d and unpack it. Edit the OpenSSL Makefiles + per PROBLEMS. Then: + + ./config no-shared no-asm --prefix=/usr/local + make + make test + sudo make install + make clean + ./Configure no-shared no-asm --prefix=/usr/local darwin-ppc-cc + make build_libs "CC=cc -arch ppc" + lipo -info lib* + mkdir -p build/ppc + mv lib* build/ppc + make clean + ./Configure no-shared no-asm --prefix=/usr/local darwin-i386-cc + make build_libs "CC=cc -arch i386" + lipo -info lib* + mkdir -p build/i386 + mv lib* build/i386/ + /bin/ls -1 build/i386/ > libnames.tmp + mkdir universal + + Create a script in the OpenSSL directory called 'make_universal', with these + contents: + + #!/bin/sh + for lib in `cat libnames.tmp`; do + lipo -create build/*/$lib -output universal/$lib + done + exit 0 + + Then: + + sh make_universal + lipo -info universal/lib* + sudo cp universal/lib* /usr/local/lib + lipo -info /usr/local/lib/lib{crypto,ssl}* + cd ../m2crypto-0.17 + + Then edit the m2crypto setup.py and uncomment the extra_link_args line at + the end. + + python setup.py build build_ext --openssl=/usr/local + sudo python setup.py install build_ext --openssl=/usr/local + + @@ -0,0 +1,26 @@ +Copyright (c) 1999-2004 Ng Pheng Siong. All rights reserved. + +Portions copyright (c) 2004-2006 Open Source Applications Foundation. +All rights reserved. + +Portions copyright (c) 2005-2006 Vrije Universiteit Amsterdam. +All rights reserved. + +Copyright (c) 2008-2010 Heikki Toivonen. All rights reserved. + +Permission to use, copy, modify, and distribute this software and its +documentation for any purpose and without fee is hereby granted, +provided that the above copyright notice appear in all copies and that +both that copyright notice and this permission notice appear in +supporting documentation. + +THE AUTHOR PROVIDES THIS SOFTWARE ``AS IS'' AND ANY EXPRESSED OR +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT +NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/M2Crypto.egg-info/PKG-INFO b/M2Crypto.egg-info/PKG-INFO new file mode 100644 index 0000000..3972df5 --- /dev/null +++ b/M2Crypto.egg-info/PKG-INFO @@ -0,0 +1,23 @@ +Metadata-Version: 1.0 +Name: M2Crypto +Version: 0.21.1 +Summary: M2Crypto: A Python crypto and SSL toolkit +Home-page: http://chandlerproject.org/Projects/MeTooCrypto +Author: Heikki Toivonen +Author-email: heikki@osafoundation.org +License: BSD-style license +Description: M2Crypto is the most complete Python wrapper for OpenSSL featuring RSA, DSA, + DH, EC, HMACs, message digests, symmetric ciphers (including AES); SSL + functionality to implement clients and servers; HTTPS extensions to Python's + httplib, urllib, and xmlrpclib; unforgeable HMAC'ing AuthCookies for web + session management; FTP/TLS client and server; S/MIME; ZServerSSL: A HTTPS + server for Zope and ZSmime: An S/MIME messenger for Zope. M2Crypto can also be + used to provide SSL for Twisted. +Platform: any +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: C +Classifier: Programming Language :: Python +Classifier: Topic :: Security :: Cryptography +Classifier: Topic :: Software Development :: Libraries :: Python Modules diff --git a/M2Crypto.egg-info/SOURCES.txt b/M2Crypto.egg-info/SOURCES.txt new file mode 100644 index 0000000..dffdb58 --- /dev/null +++ b/M2Crypto.egg-info/SOURCES.txt @@ -0,0 +1,354 @@ +CHANGES +INSTALL +LICENCE +README +epydoc.conf +fedora_setup.sh +pack.py +setup.cfg +setup.py +M2Crypto/ASN1.py +M2Crypto/AuthCookie.py +M2Crypto/BIO.py +M2Crypto/BN.py +M2Crypto/DH.py +M2Crypto/DSA.py +M2Crypto/EC.py +M2Crypto/EVP.py +M2Crypto/Engine.py +M2Crypto/Err.py +M2Crypto/RC4.py +M2Crypto/RSA.py +M2Crypto/Rand.py +M2Crypto/SMIME.py +M2Crypto/X509.py +M2Crypto/__init__.py +M2Crypto/callback.py +M2Crypto/ftpslib.py +M2Crypto/httpslib.py +M2Crypto/m2.py +M2Crypto/m2urllib.py +M2Crypto/m2urllib2.py +M2Crypto/m2xmlrpclib.py +M2Crypto/threading.py +M2Crypto/util.py +M2Crypto.egg-info/PKG-INFO +M2Crypto.egg-info/SOURCES.txt +M2Crypto.egg-info/dependency_links.txt +M2Crypto.egg-info/top_level.txt +M2Crypto/PGP/PublicKey.py +M2Crypto/PGP/PublicKeyRing.py +M2Crypto/PGP/RSA.py +M2Crypto/PGP/__init__.py +M2Crypto/PGP/constants.py +M2Crypto/PGP/packet.py +M2Crypto/SSL/Checker.py +M2Crypto/SSL/Cipher.py +M2Crypto/SSL/Connection.py +M2Crypto/SSL/Context.py +M2Crypto/SSL/SSLServer.py +M2Crypto/SSL/Session.py +M2Crypto/SSL/TwistedProtocolWrapper.py +M2Crypto/SSL/__init__.py +M2Crypto/SSL/cb.py +M2Crypto/SSL/ssl_dispatcher.py +M2Crypto/SSL/timeout.py +SWIG/Makefile +SWIG/Makefile.mw +SWIG/Makefile.osx +SWIG/_aes.i +SWIG/_asn1.i +SWIG/_bio.i +SWIG/_bn.i +SWIG/_dh.i +SWIG/_dsa.i +SWIG/_ec.i +SWIG/_engine.i +SWIG/_evp.i +SWIG/_lib.h +SWIG/_lib.i +SWIG/_m2crypto.def +SWIG/_m2crypto.i +SWIG/_objects.i +SWIG/_pkcs7.i +SWIG/_rand.i +SWIG/_rc4.i +SWIG/_rsa.i +SWIG/_ssl.i +SWIG/_threads.i +SWIG/_util.i +SWIG/_x509.i +contrib/README +contrib/SimpleX509create.README +contrib/SimpleX509create.py +contrib/dave.README +contrib/dave.patch +contrib/dispatcher.README +contrib/dispatcher.py +contrib/isaac.README +contrib/isaac.httpslib.py +contrib/m2crypto.spec +contrib/smimeplus.README +contrib/smimeplus.py +demo/bio_mem_rw.py +demo/dhtest.py +demo/dsa1024pvtkey.pem +demo/dsa_bench.py +demo/dsatest.pem +demo/dsatest.py +demo/rsa.priv.pem +demo/rsa.priv0.pem +demo/rsa.pub.pem +demo/rsa1024pvtkey.pem +demo/rsa_bench.py +demo/rsatest.py +demo/CipherSaber/CipherSaber.py +demo/CipherSaber/cstest1.cs1 +demo/Zope/ca.pem +demo/Zope/dh1024.pem +demo/Zope/server.pem +demo/Zope/starts +demo/Zope/starts.bat +demo/Zope/z2s.py +demo/Zope/z2s.py.diff +demo/Zope/ZServer/HTTPS_Server.py +demo/Zope/ZServer/__init__.py +demo/Zope/ZServer/medusa/ftps_server.py +demo/Zope/ZServer/medusa/https_server.py +demo/Zope/lib/python/Products/GuardedFile/GuardedFile.py +demo/Zope/lib/python/Products/GuardedFile/README.txt +demo/Zope/lib/python/Products/GuardedFile/TODO.txt +demo/Zope/lib/python/Products/GuardedFile/__init__.py +demo/Zope/lib/python/Products/GuardedFile/add.dtml +demo/Zope/lib/python/Products/GuardedFile/refresh.txt +demo/Zope/lib/python/Products/GuardedFile/version.txt +demo/Zope/lib/python/Products/ZSmime/README.txt +demo/Zope/lib/python/Products/ZSmime/SmimeTag.py +demo/Zope/lib/python/Products/ZSmime/__init__.py +demo/Zope/lib/python/Products/ZSmime/version.txt +demo/Zope/utilities/x509_user.py +demo/Zope27/INSTALL.txt +demo/Zope27/install_dir/lib/python/ZServer/HTTPS_Server.py +demo/Zope27/install_dir/lib/python/ZServer/__init__.py.patch +demo/Zope27/install_dir/lib/python/ZServer/component.xml.patch +demo/Zope27/install_dir/lib/python/ZServer/datatypes.py.patch +demo/Zope27/install_dir/lib/python/ZServer/medusa/https_server.py +demo/Zope27/instance_home/README.txt.patch +demo/Zope27/instance_home/etc/zope.conf.patch +demo/Zope27/instance_home/ssl/ca.pem +demo/Zope27/instance_home/ssl/dh1024.pem +demo/Zope27/instance_home/ssl/server.pem +demo/ZopeX3/INSTALL.txt +demo/ZopeX3/install_dir/lib/python/zope/app/server/configure.zcml.patch +demo/ZopeX3/install_dir/lib/python/zope/app/server/https.py +demo/ZopeX3/install_dir/lib/python/zope/server/http/https_server.py +demo/ZopeX3/install_dir/lib/python/zope/server/http/https_serverchannel.py +demo/ZopeX3/install_dir/lib/python/zope/server/http/publisherhttps_server.py +demo/ZopeX3/instance_home/etc/zope.conf.patch +demo/ZopeX3/instance_home/ssl/ca.pem +demo/ZopeX3/instance_home/ssl/dh1024.pem +demo/ZopeX3/instance_home/ssl/server.pem +demo/ec/ecdhtest.py +demo/ec/ecdsa_bench.py +demo/ec/ecdsatest.pem +demo/ec/ecdsatest.py +demo/ec/secp160r1pvtkey.pem +demo/https.howto/ca.pem +demo/https.howto/dh1024.pem +demo/https.howto/get_https.py +demo/https.howto/https_cli.py +demo/https.howto/orig_https_srv.py +demo/https.howto/server.pem +demo/medusa/00_README +demo/medusa/START.py +demo/medusa/START_xmlrpc.py +demo/medusa/asynchat.py +demo/medusa/asyncore.py +demo/medusa/auth_handler.py +demo/medusa/ca.pem +demo/medusa/counter.py +demo/medusa/default_handler.py +demo/medusa/dh1024.pem +demo/medusa/filesys.py +demo/medusa/ftp_server.py +demo/medusa/ftps_server.py +demo/medusa/http_date.py +demo/medusa/http_server.py +demo/medusa/https_server.py +demo/medusa/index.html +demo/medusa/logger.py +demo/medusa/m_syslog.py +demo/medusa/medusa_gif.py +demo/medusa/mime_type_table.py +demo/medusa/poison_handler.py +demo/medusa/producers.py +demo/medusa/put_handler.py +demo/medusa/redirecting_handler.py +demo/medusa/server.pem +demo/medusa/status_handler.py +demo/medusa/virtual_handler.py +demo/medusa/xmlrpc_handler.py +demo/medusa054/00_README +demo/medusa054/START.py +demo/medusa054/START_xmlrpc.py +demo/medusa054/ca.pem +demo/medusa054/counter.py +demo/medusa054/default_handler.py +demo/medusa054/dh1024.pem +demo/medusa054/filesys.py +demo/medusa054/ftp_server.py +demo/medusa054/ftps_server.py +demo/medusa054/http_date.py +demo/medusa054/http_server.py +demo/medusa054/https_server.py +demo/medusa054/index.html +demo/medusa054/logger.py +demo/medusa054/m_syslog.py +demo/medusa054/medusa_gif.py +demo/medusa054/poison_handler.py +demo/medusa054/producers.py +demo/medusa054/server.pem +demo/medusa054/status_handler.py +demo/medusa054/xmlrpc_handler.py +demo/perf/memio.py +demo/perf/sha1.py +demo/pgp/pgpstep.py +demo/pgp/pubring.pgp +demo/pgp/secring.pgp +demo/pkcs7/pkcs7-thawte.pem +demo/pkcs7/test.py +demo/smime/README +demo/smime/ca.pem +demo/smime/clear.p7 +demo/smime/client.p12 +demo/smime/client.pem +demo/smime/client2.pem +demo/smime/m2.se.p7 +demo/smime/ns.p7 +demo/smime/ns.se.p7 +demo/smime/opaque.p7 +demo/smime/sendsmime.py +demo/smime/test.py +demo/smime/unsmime.py +demo/smime.howto/README +demo/smime.howto/decrypt.py +demo/smime.howto/dv.py +demo/smime.howto/encrypt.p7 +demo/smime.howto/encrypt.py +demo/smime.howto/recipient.pem +demo/smime.howto/recipient_key.pem +demo/smime.howto/se.p7 +demo/smime.howto/se.py +demo/smime.howto/sendsmime.py +demo/smime.howto/sign.p7 +demo/smime.howto/sign.py +demo/smime.howto/signer.pem +demo/smime.howto/signer_key.pem +demo/smime.howto/verify.py +demo/ssl/README +demo/ssl/c.py +demo/ssl/c_bio.py +demo/ssl/ca.der +demo/ssl/ca.pem +demo/ssl/client.p12 +demo/ssl/client.pem +demo/ssl/dh1024.pem +demo/ssl/echo-eg.py +demo/ssl/echo.py +demo/ssl/echod-async.py +demo/ssl/echod-eg1.py +demo/ssl/echod-forking.py +demo/ssl/echod-iterative.py +demo/ssl/echod-thread.py +demo/ssl/echod-threading.py +demo/ssl/echod_lib.py +demo/ssl/ftp_tls.py +demo/ssl/http_cli_20.py +demo/ssl/https_cli.py +demo/ssl/https_cli_async.py +demo/ssl/https_srv.py +demo/ssl/myapp.py +demo/ssl/s_client.py +demo/ssl/s_server.py +demo/ssl/server.pem +demo/ssl/server3.py +demo/ssl/sess.py +demo/ssl/sess2.py +demo/ssl/sess2.ssldump.out +demo/ssl/socklib.py +demo/ssl/somelib.py +demo/ssl/ss.py +demo/ssl/twistedsslclient.py +demo/ssl/twistedsslserver.py +demo/ssl/xmlrpc_cli.py +demo/ssl/xmlrpc_srv.py +demo/tinderbox/build_lib.py +demo/tinderbox/killableprocess.py +demo/tinderbox/slave.py +demo/tinderbox/winprocess.py +demo/x509/ca.py +demo/x509/certdata2pem.py +demo/x509/client2.pem +demo/x509/demo1.py +demo/x509/proxy_destroy.py +demo/x509/proxy_info.py +demo/x509/proxy_init.py +demo/x509/proxylib.py +demo/x509/server-expired.pem +demo/x509/server.pem +demo/x509/x509auth.py +doc/ZServerSSL-HOWTO.html +doc/howto.ca.html +doc/howto.smime.html +doc/howto.ssl.html +tests/README +tests/__init__.py +tests/alltests.py +tests/ca.pem +tests/der_encoded_seq.b64 +tests/dhparams.pem +tests/dsa.param.pem +tests/dsa.priv.pem +tests/dsa.pub.pem +tests/ec.priv.pem +tests/ec.pub.pem +tests/fips.py +tests/long_serial_cert.pem +tests/pubring.pgp +tests/recipient.pem +tests/recipient_key.pem +tests/rsa.priv.pem +tests/rsa.priv2.pem +tests/rsa.pub.pem +tests/server.pem +tests/signer.pem +tests/signer_key.pem +tests/test_asn1.py +tests/test_authcookie.py +tests/test_bio.py +tests/test_bio_file.py +tests/test_bio_iobuf.py +tests/test_bio_membuf.py +tests/test_bio_ssl.py +tests/test_bn.py +tests/test_dh.py +tests/test_dsa.py +tests/test_ec_curves.py +tests/test_ecdh.py +tests/test_ecdsa.py +tests/test_engine.py +tests/test_evp.py +tests/test_obj.py +tests/test_pgp.py +tests/test_rand.py +tests/test_rc4.py +tests/test_rsa.py +tests/test_smime.py +tests/test_ssl.py +tests/test_ssl_offline.py +tests/test_ssl_win.py +tests/test_threading.py +tests/test_x509.py +tests/thawte.pem +tests/x509.der +tests/x509.pem
\ No newline at end of file diff --git a/M2Crypto.egg-info/dependency_links.txt b/M2Crypto.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/M2Crypto.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/M2Crypto.egg-info/top_level.txt b/M2Crypto.egg-info/top_level.txt new file mode 100644 index 0000000..fa4a704 --- /dev/null +++ b/M2Crypto.egg-info/top_level.txt @@ -0,0 +1 @@ +M2Crypto diff --git a/M2Crypto/ASN1.py b/M2Crypto/ASN1.py new file mode 100644 index 0000000..09d9e9f --- /dev/null +++ b/M2Crypto/ASN1.py @@ -0,0 +1,192 @@ +""" +M2Crypto wrapper for OpenSSL ASN1 API. + +Copyright (c) 1999-2004 Ng Pheng Siong. All rights reserved. + +Portions created by Open Source Applications Foundation (OSAF) are +Copyright (C) 2005 OSAF. All Rights Reserved. +""" + +import time, datetime + +import BIO +import m2 + +MBSTRING_FLAG = 0x1000 +MBSTRING_ASC = MBSTRING_FLAG | 1 +MBSTRING_BMP = MBSTRING_FLAG | 2 + + +class ASN1_Integer: + + m2_asn1_integer_free = m2.asn1_integer_free + + def __init__(self, asn1int, _pyfree=0): + self.asn1int = asn1int + self._pyfree = _pyfree + + def __cmp__(self, other): + return m2.asn1_integer_cmp(self.asn1int, other.asn1int) + + def __del__(self): + if self._pyfree: + self.m2_asn1_integer_free(self.asn1int) + + +class ASN1_String: + + m2_asn1_string_free = m2.asn1_string_free + + def __init__(self, asn1str, _pyfree=0): + self.asn1str = asn1str + self._pyfree = _pyfree + + def __str__(self): + buf = BIO.MemoryBuffer() + m2.asn1_string_print( buf.bio_ptr(), self.asn1str ) + return buf.read_all() + + def __del__(self): + if getattr(self, '_pyfree', 0): + self.m2_asn1_string_free(self.asn1str) + + def _ptr(self): + return self.asn1str + + def as_text(self, flags=0): + buf = BIO.MemoryBuffer() + m2.asn1_string_print_ex( buf.bio_ptr(), self.asn1str, flags) + return buf.read_all() + + +class ASN1_Object: + + m2_asn1_object_free = m2.asn1_object_free + + def __init__(self, asn1obj, _pyfree=0): + self.asn1obj = asn1obj + self._pyfree = _pyfree + + def __del__(self): + if self._pyfree: + self.m2_asn1_object_free(self.asn1obj) + + def _ptr(self): + return self.asn1obj + +class _UTC(datetime.tzinfo): + def tzname(self, dt): + return "UTC" + + def dst(self, dt): + return datetime.timedelta(0) + + def utcoffset(self, dt): + return datetime.timedelta(0) + + def __repr__(self): + return "<Timezone: %s>" % self.tzname(None) +UTC = _UTC() + + +class LocalTimezone(datetime.tzinfo): + """ Localtimezone from datetime manual """ + def __init__(self): + self._stdoffset = datetime.timedelta(seconds = -time.timezone) + if time.daylight: + self._dstoffset = datetime.timedelta(seconds = -time.altzone) + else: + self._dstoffset = self._stdoffset + self._dstdiff = self._dstoffset - self._stdoffset + + + def utcoffset(self, dt): + if self._isdst(dt): + return self._dstoffset + else: + return self._stdoffset + + def dst(self, dt): + if self._isdst(dt): + return self._dstdiff + else: + return datetime.timedelta(0) + + def tzname(self, dt): + return time.tzname[self._isdst(dt)] + + def _isdst(self, dt): + tt = (dt.year, dt.month, dt.day, + dt.hour, dt.minute, dt.second, + dt.weekday(), 0, -1) + stamp = time.mktime(tt) + tt = time.localtime(stamp) + return tt.tm_isdst > 0 + + +class ASN1_UTCTIME: + _ssl_months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", + "Sep", "Oct", "Nov", "Dec"] + m2_asn1_utctime_free = m2.asn1_utctime_free + + def __init__(self, asn1_utctime=None, _pyfree=0): + if asn1_utctime is not None: + assert m2.asn1_utctime_type_check(asn1_utctime), "'asn1_utctime' type error'" + self.asn1_utctime = asn1_utctime + self._pyfree = _pyfree + else: + self.asn1_utctime = m2.asn1_utctime_new () + self._pyfree = 1 + + def __del__(self): + if getattr(self, '_pyfree', 0): + self.m2_asn1_utctime_free(self.asn1_utctime) + + def __str__(self): + assert m2.asn1_utctime_type_check(self.asn1_utctime), "'asn1_utctime' type error'" + buf = BIO.MemoryBuffer() + m2.asn1_utctime_print( buf.bio_ptr(), self.asn1_utctime ) + return buf.read_all() + + def _ptr(self): + assert m2.asn1_utctime_type_check(self.asn1_utctime), "'asn1_utctime' type error'" + return self.asn1_utctime + + def set_string (self, string): + """ + Set time from UTC string. + """ + assert m2.asn1_utctime_type_check(self.asn1_utctime), "'asn1_utctime' type error'" + return m2.asn1_utctime_set_string( self.asn1_utctime, string ) + + def set_time (self, time): + """ + Set time from seconds since epoch (long). + """ + assert m2.asn1_utctime_type_check(self.asn1_utctime), "'asn1_utctime' type error'" + return m2.asn1_utctime_set( self.asn1_utctime, time ) + + def get_datetime(self): + date = str(self) + + timezone = None + if ' ' not in date: + raise ValueError("Invalid date: %s" % date) + month, rest = date.split(' ', 1) + if month not in self._ssl_months: + raise ValueError("Invalid date %s: Invalid month: %s" % (date, m)) + if rest.endswith(' GMT'): + timezone = UTC + rest = rest[:-4] + tm = list(time.strptime(rest, "%d %H:%M:%S %Y"))[:6] + tm[1] = self._ssl_months.index(month) + 1 + tm.append(0) + tm.append(timezone) + return datetime.datetime(*tm) + + def set_datetime(self, date): + local = LocalTimezone() + if date.tzinfo is None: + date = date.replace(tzinfo=local) + date = date.astimezone(local) + return self.set_time(int(time.mktime(date.timetuple()))) diff --git a/M2Crypto/AuthCookie.py b/M2Crypto/AuthCookie.py new file mode 100644 index 0000000..d401708 --- /dev/null +++ b/M2Crypto/AuthCookie.py @@ -0,0 +1,114 @@ +"""Secure Authenticator Cookies + +Copyright (c) 1999-2002 Ng Pheng Siong. All rights reserved.""" + +# M2Crypto +import Rand, m2 + +# Python. Cookie is bundled with Python 2.x. +import Cookie, binascii, re, time + + +_MIX_FORMAT = 'exp=%s&data=%s&digest=' +_MIX_RE = re.compile('exp=(\d+\.\d+)&data=(.+)&digest=(\S*)') + +def mix(expiry, data, format=_MIX_FORMAT): + return format % (repr(expiry), data) + +def unmix(dough, regex=_MIX_RE): + mo = regex.match(dough) + if mo: + return float(mo.group(1)), mo.group(2) + else: + return None + +def unmix3(dough, regex=_MIX_RE): + mo = regex.match(dough) + if mo: + return float(mo.group(1)), mo.group(2), mo.group(3) + else: + return None + + +_TOKEN = '_M2AUTH_' + +class AuthCookieJar: + + _keylen = 20 + + def __init__(self): + self._key = Rand.rand_bytes(self._keylen) + + def _hmac(self, key, data): + return binascii.b2a_base64(m2.hmac(key, data, m2.sha1()))[:-1] + + def makeCookie(self, expiry, data): + dough = mix(expiry, data) + return AuthCookie(expiry, data, dough, self._hmac(self._key, dough)) + + def isGoodCookie(self, cookie): + assert isinstance(cookie, AuthCookie) + if cookie.isExpired(): + return 0 + c = self.makeCookie(cookie._expiry, cookie._data) + return (c._expiry == cookie._expiry) \ + and (c._data == cookie._data) \ + and (c._mac == cookie._mac) \ + and (c.output() == cookie.output()) + + def isGoodCookieString(self, cookie_str): + c = Cookie.SmartCookie() + c.load(cookie_str) + if not c.has_key(_TOKEN): + return 0 + undough = unmix3(c[_TOKEN].value) + if undough is None: + return 0 + exp, data, mac = undough + c2 = self.makeCookie(exp, data) + return (not c2.isExpired()) and (c2._mac == mac) + + +class AuthCookie: + + def __init__(self, expiry, data, dough, mac): + self._expiry = expiry + self._data = data + self._mac = mac + self._cookie = Cookie.SmartCookie() + self._cookie[_TOKEN] = '%s%s' % (dough, mac) + self._name = '%s%s' % (dough, mac) # XXX WebKit only. + + def expiry(self): + """Return the cookie's expiry time.""" + return self._expiry + + def data(self): + """Return the data portion of the cookie.""" + return self._data + + def mac(self): + """Return the cookie's MAC.""" + return self._mac + + def output(self): + """Return the cookie's output in "Set-Cookie" format.""" + return self._cookie.output() + + def value(self): + """Return the cookie's output minus the "Set-Cookie: " portion. + """ + return self._cookie[_TOKEN].value + + def isExpired(self): + """Return 1 if the cookie has expired, 0 otherwise.""" + return (time.time() > self._expiry) + + # XXX Following methods are for WebKit only. These should be pushed + # to WKAuthCookie. + def name(self): + return self._name + + def headerValue(self): + return self.value() + diff --git a/M2Crypto/BIO.py b/M2Crypto/BIO.py new file mode 100644 index 0000000..11dbce4 --- /dev/null +++ b/M2Crypto/BIO.py @@ -0,0 +1,286 @@ +"""M2Crypto wrapper for OpenSSL BIO API. + +Copyright (c) 1999-2004 Ng Pheng Siong. All rights reserved.""" + +import m2 + +# Deprecated +from m2 import bio_do_handshake as bio_do_ssl_handshake + +from cStringIO import StringIO + +class BIOError(Exception): pass + +m2.bio_init(BIOError) + +class BIO: + + """Abstract object interface to the BIO API.""" + + m2_bio_free = m2.bio_free + + def __init__(self, bio=None, _pyfree=0, _close_cb=None): + self.bio = bio + self._pyfree = _pyfree + self._close_cb = _close_cb + self.closed = 0 + self.write_closed = 0 + + def __del__(self): + if self._pyfree: + self.m2_bio_free(self.bio) + + def _ptr(self): + return self.bio + + # Deprecated. + bio_ptr = _ptr + + def fileno(self): + return m2.bio_get_fd(self.bio) + + def readable(self): + return not self.closed + + def read(self, size=None): + if not self.readable(): + raise IOError, 'cannot read' + if size is None: + buf = StringIO() + while 1: + data = m2.bio_read(self.bio, 4096) + if not data: break + buf.write(data) + return buf.getvalue() + elif size == 0: + return '' + elif size < 0: + raise ValueError, 'read count is negative' + else: + return m2.bio_read(self.bio, size) + + def readline(self, size=4096): + if not self.readable(): + raise IOError, 'cannot read' + buf = m2.bio_gets(self.bio, size) + return buf + + def readlines(self, sizehint='ignored'): + if not self.readable(): + raise IOError, 'cannot read' + lines=[] + while 1: + buf=m2.bio_gets(self.bio, 4096) + if buf is None: + break + lines.append(buf) + return lines + + def writeable(self): + return (not self.closed) and (not self.write_closed) + + def write(self, data): + if not self.writeable(): + raise IOError, 'cannot write' + return m2.bio_write(self.bio, data) + + def write_close(self): + self.write_closed = 1 + + def flush(self): + m2.bio_flush(self.bio) + + def reset(self): + """ + Sets the bio to its initial state + """ + return m2.bio_reset(self.bio) + + def close(self): + self.closed = 1 + if self._close_cb: + self._close_cb() + + def should_retry(self): + """ + Can the call be attempted again, or was there an error + ie do_handshake + + """ + return m2.bio_should_retry(self.bio) + + def should_read(self): + """ + Returns whether the cause of the condition is the bio + should read more data + """ + return m2.bio_should_read(self.bio) + + def should_write(self): + """ + Returns whether the cause of the condition is the bio + should write more data + """ + return m2.bio_should_write(self.bio) + +class MemoryBuffer(BIO): + + """ + Object interface to BIO_s_mem. + + Empirical testing suggests that this class performs less well than cStringIO, + because cStringIO is implemented in C, whereas this class is implemented in + Python. Thus, the recommended practice is to use cStringIO for regular work and + convert said cStringIO object to a MemoryBuffer object only when necessary. + """ + + def __init__(self, data=None): + BIO.__init__(self) + self.bio = m2.bio_new(m2.bio_s_mem()) + self._pyfree = 1 + if data is not None: + m2.bio_write(self.bio, data) + + def __len__(self): + return m2.bio_ctrl_pending(self.bio) + + def read(self, size=0): + if not self.readable(): + raise IOError, 'cannot read' + if size: + return m2.bio_read(self.bio, size) + else: + return m2.bio_read(self.bio, m2.bio_ctrl_pending(self.bio)) + + # Backwards-compatibility. + getvalue = read_all = read + + def write_close(self): + self.write_closed = 1 + m2.bio_set_mem_eof_return(self.bio, 0) + + close = write_close + + +class File(BIO): + + """ + Object interface to BIO_s_fp. + + This class interfaces Python to OpenSSL functions that expect BIO *. For + general file manipulation in Python, use Python's builtin file object. + """ + + def __init__(self, pyfile, close_pyfile=1): + BIO.__init__(self, _pyfree=1) + self.pyfile = pyfile + self.close_pyfile = close_pyfile + self.bio = m2.bio_new_fp(pyfile, 0) + + def close(self): + self.closed = 1 + if self.close_pyfile: + self.pyfile.close() + +def openfile(filename, mode='rb'): + return File(open(filename, mode)) + + +class IOBuffer(BIO): + + """ + Object interface to BIO_f_buffer. + + Its principal function is to be BIO_push()'ed on top of a BIO_f_ssl, so + that makefile() of said underlying SSL socket works. + """ + + m2_bio_pop = m2.bio_pop + m2_bio_free = m2.bio_free + + def __init__(self, under_bio, mode='rwb', _pyfree=1): + BIO.__init__(self, _pyfree=_pyfree) + self.io = m2.bio_new(m2.bio_f_buffer()) + self.bio = m2.bio_push(self.io, under_bio._ptr()) + # This reference keeps the underlying BIO alive while we're not closed. + self._under_bio = under_bio + if 'w' in mode: + self.write_closed = 0 + else: + self.write_closed = 1 + + def __del__(self): + if getattr(self, '_pyfree', 0): + self.m2_bio_pop(self.bio) + self.m2_bio_free(self.io) + + def close(self): + BIO.close(self) + + +class CipherStream(BIO): + + """ + Object interface to BIO_f_cipher. + """ + + SALT_LEN = m2.PKCS5_SALT_LEN + + m2_bio_pop = m2.bio_pop + m2_bio_free = m2.bio_free + + def __init__(self, obio): + BIO.__init__(self, _pyfree=1) + self.obio = obio + self.bio = m2.bio_new(m2.bio_f_cipher()) + self.closed = 0 + + def __del__(self): + if not getattr(self, 'closed', 1): + self.close() + + def close(self): + self.m2_bio_pop(self.bio) + self.m2_bio_free(self.bio) + self.closed = 1 + + def write_close(self): + self.obio.write_close() + + def set_cipher(self, algo, key, iv, op): + cipher = getattr(m2, algo, None) + if cipher is None: + raise ValueError, ('unknown cipher', algo) + m2.bio_set_cipher(self.bio, cipher(), key, iv, op) + m2.bio_push(self.bio, self.obio._ptr()) + + +class SSLBio(BIO): + """ + Object interface to BIO_f_ssl + """ + def __init__(self, _pyfree=1): + BIO.__init__(self, _pyfree) + self.bio = m2.bio_new(m2.bio_f_ssl()) + self.closed = 0 + + + def set_ssl(self, conn, close_flag=m2.bio_noclose): + """ + Sets the bio to the SSL pointer which is + contained in the connection object. + """ + self._pyfree = 0 + m2.bio_set_ssl(self.bio, conn.ssl, close_flag) + if close_flag == m2.bio_noclose: + conn.set_ssl_close_flag(m2.bio_close) + + def do_handshake(self): + """ + Do the handshake. + + Return 1 if the handshake completes + Return 0 or a negative number if there is a problem + """ + return m2.bio_do_handshake(self.bio) + diff --git a/M2Crypto/BN.py b/M2Crypto/BN.py new file mode 100755 index 0000000..ec741c0 --- /dev/null +++ b/M2Crypto/BN.py @@ -0,0 +1,47 @@ +""" +M2Crypto wrapper for OpenSSL BN (BIGNUM) API. + +Copyright (c) 2005 Open Source Applications Foundation. All rights reserved. +""" + +import m2 + +def rand(bits, top=-1, bottom=0): + """ + Generate cryptographically strong random number. + + @param bits: Length of random number in bits. + @param top: If -1, the most significant bit can be 0. If 0, the most + significant bit is 1, and if 1, the two most significant + bits will be 1. + @param bottom: If bottom is true, the number will be odd. + """ + return m2.bn_rand(bits, top, bottom) + + +def rand_range(range): + """ + Generate a random number in a range. + + @param range: Upper limit for range. + @return: A random number in the range [0, range) + """ + return m2.bn_rand_range(range) + + +def randfname(length): + """ + Return a random filename, which is simply a string where all + the characters are from the set [a-zA-Z0-9]. + + @param length: Length of filename to return. + @type length: int + @return: random filename string + """ + letters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567890' + lettersLen = len(letters) + fname = [] + for x in range(length): + fname += [letters[m2.bn_rand_range(lettersLen)]] + + return ''.join(fname) diff --git a/M2Crypto/DH.py b/M2Crypto/DH.py new file mode 100644 index 0000000..4d454ef --- /dev/null +++ b/M2Crypto/DH.py @@ -0,0 +1,96 @@ +"""M2Crypto wrapper for OpenSSL DH API. + +Copyright (c) 1999-2003 Ng Pheng Siong. All rights reserved.""" + +from util import genparam_callback +import BIO, Err, m2 + +class DHError(Exception): pass + +m2.dh_init(DHError) + +class DH: + + """ + Object interface to the Diffie-Hellman key exchange + protocol. + """ + + m2_dh_free = m2.dh_free + + def __init__(self, dh, _pyfree=0): + assert m2.dh_type_check(dh) + self.dh = dh + self._pyfree = _pyfree + + def __del__(self): + if getattr(self, '_pyfree', 0): + self.m2_dh_free(self.dh) + + def __len__(self): + assert m2.dh_type_check(self.dh), "'dh' type error" + return m2.dh_size(self.dh) + + def __getattr__(self, name): + if name in ('p', 'g', 'pub', 'priv'): + method = getattr(m2, 'dh_get_%s' % (name,)) + assert m2.dh_type_check(self.dh), "'dh' type error" + return method(self.dh) + else: + raise AttributeError + + def __setattr__(self, name, value): + if name in ('p', 'g'): + raise DHError, 'set (p, g) via set_params()' + elif name in ('pub','priv'): + raise DHError, 'generate (pub, priv) via gen_key()' + else: + self.__dict__[name] = value + + def _ptr(self): + return self.dh + + def check_params(self): + assert m2.dh_type_check(self.dh), "'dh' type error" + return m2.dh_check(self.dh) + + def gen_key(self): + assert m2.dh_type_check(self.dh), "'dh' type error" + m2.dh_generate_key(self.dh) + + def compute_key(self, pubkey): + assert m2.dh_type_check(self.dh), "'dh' type error" + return m2.dh_compute_key(self.dh, pubkey) + + def print_params(self, bio): + assert m2.dh_type_check(self.dh), "'dh' type error" + return m2.dhparams_print(bio._ptr(), self.dh) + + +def gen_params(plen, g, callback=genparam_callback): + return DH(m2.dh_generate_parameters(plen, g, callback), 1) + + +def load_params(file): + bio = BIO.openfile(file) + return load_params_bio(bio) + + +def load_params_bio(bio): + return DH(m2.dh_read_parameters(bio._ptr()), 1) + + +def set_params(p, g): + dh = m2.dh_new() + m2.dh_set_p(dh, p) + m2.dh_set_g(dh, g) + return DH(dh, 1) + + +#def free_params(cptr): +# m2.dh_free(cptr) + + +DH_GENERATOR_2 = m2.DH_GENERATOR_2 +DH_GENERATOR_5 = m2.DH_GENERATOR_5 + diff --git a/M2Crypto/DSA.py b/M2Crypto/DSA.py new file mode 100644 index 0000000..db1f2ff --- /dev/null +++ b/M2Crypto/DSA.py @@ -0,0 +1,439 @@ +""" + M2Crypto wrapper for OpenSSL DSA API. + + Copyright (c) 1999-2003 Ng Pheng Siong. All rights reserved. + + Portions created by Open Source Applications Foundation (OSAF) are + Copyright (C) 2004 OSAF. All Rights Reserved. +""" + +import sys +import util, BIO, m2 + +class DSAError(Exception): pass + +m2.dsa_init(DSAError) + +class DSA: + + """ + This class is a context supporting DSA key and parameter + values, signing and verifying. + + Simple example:: + + from M2Crypto import EVP, DSA, util + + message = 'Kilroy was here!' + md = EVP.MessageDigest('sha1') + md.update(message) + digest = md.final() + + dsa = DSA.gen_params(1024) + dsa.gen_key() + r, s = dsa.sign(digest) + good = dsa.verify(digest, r, s) + if good: + print ' ** success **' + else: + print ' ** verification failed **' + """ + + m2_dsa_free = m2.dsa_free + + def __init__(self, dsa, _pyfree=0): + """ + Use one of the factory functions to create an instance. + """ + assert m2.dsa_type_check(dsa), "'dsa' type error" + self.dsa = dsa + self._pyfree = _pyfree + + def __del__(self): + if getattr(self, '_pyfree', 0): + self.m2_dsa_free(self.dsa) + + def __len__(self): + """ + Return the key length. + + @rtype: int + @return: the DSA key length in bits + """ + assert m2.dsa_type_check(self.dsa), "'dsa' type error" + return m2.dsa_keylen(self.dsa) + + def __getattr__(self, name): + """ + Return specified DSA parameters and key values. + + @type name: str + @param name: name of variable to be returned. Must be + one of 'p', 'q', 'g', 'pub', 'priv'. + @rtype: str + @return: value of specified variable (a "byte string") + """ + if name in ['p', 'q', 'g', 'pub', 'priv']: + method = getattr(m2, 'dsa_get_%s' % (name,)) + assert m2.dsa_type_check(self.dsa), "'dsa' type error" + return method(self.dsa) + else: + raise AttributeError + + def __setattr__(self, name, value): + if name in ['p', 'q', 'g']: + raise DSAError('set (p, q, g) via set_params()') + elif name in ['pub','priv']: + raise DSAError('generate (pub, priv) via gen_key()') + else: + self.__dict__[name] = value + + def set_params(self, p, q, g): + """ + Set new parameters. + + @warning: This does not change the private key, so it may be + unsafe to use this method. It is better to use + gen_params function to create a new DSA object. + """ + m2.dsa_set_p(self.dsa, p) + m2.dsa_set_q(self.dsa, q) + m2.dsa_set_g(self.dsa, g) + + def gen_key(self): + """ + Generate a key pair. + """ + assert m2.dsa_type_check(self.dsa), "'dsa' type error" + m2.dsa_gen_key(self.dsa) + + def save_params(self, filename): + """ + Save the DSA parameters to a file. + + @type filename: str + @param filename: Save the DSA parameters to this file. + @return: 1 (true) if successful + """ + bio = BIO.openfile(filename, 'wb') + ret = m2.dsa_write_params_bio(self.dsa, bio._ptr()) + bio.close() + return ret + + def save_params_bio(self, bio): + """ + Save DSA parameters to a BIO object. + + @type bio: M2Crypto.BIO object + @param bio: Save DSA parameters to this object. + @return: 1 (true) if successful + """ + return m2.dsa_write_params_bio(self.dsa, bio._ptr()) + + def save_key(self, filename, cipher='aes_128_cbc', + callback=util.passphrase_callback): + """ + Save the DSA key pair to a file. + + @type filename: str + @param filename: Save the DSA key pair to this file. + @type cipher: str + @param cipher: name of symmetric key algorithm and mode + to encrypt the private key. + @return: 1 (true) if successful + """ + bio = BIO.openfile(filename, 'wb') + ret = self.save_key_bio(bio, cipher, callback) + bio.close() + return ret + + def save_key_bio(self, bio, cipher='aes_128_cbc', + callback=util.passphrase_callback): + """ + Save DSA key pair to a BIO object. + + @type bio: M2Crypto.BIO object + @param bio: Save DSA parameters to this object. + @type cipher: str + @param cipher: name of symmetric key algorithm and mode + to encrypt the private key. + @return: 1 (true) if successful + """ + if cipher is None: + return m2.dsa_write_key_bio_no_cipher(self.dsa, + bio._ptr(), callback) + else: + ciph = getattr(m2, cipher, None) + if ciph is None: + raise DSAError('no such cipher: %s' % cipher) + else: + ciph = ciph() + return m2.dsa_write_key_bio(self.dsa, bio._ptr(), ciph, callback) + + def save_pub_key(self, filename): + """ + Save the DSA public key (with parameters) to a file. + + @type filename: str + @param filename: Save DSA public key (with parameters) + to this file. + @return: 1 (true) if successful + """ + bio = BIO.openfile(filename, 'wb') + ret = self.save_pub_key_bio(bio) + bio.close() + return ret + + def save_pub_key_bio(self, bio): + """ + Save DSA public key (with parameters) to a BIO object. + + @type bio: M2Crypto.BIO object + @param bio: Save DSA public key (with parameters) + to this object. + @return: 1 (true) if successful + """ + return m2.dsa_write_pub_key_bio(self.dsa, bio._ptr()) + + def sign(self, digest): + """ + Sign the digest. + + @type digest: str + @param digest: SHA-1 hash of message (same as output + from MessageDigest, a "byte string") + @rtype: tuple + @return: DSA signature, a tuple of two values, r and s, + both "byte strings". + """ + assert self.check_key(), 'key is not initialised' + return m2.dsa_sign(self.dsa, digest) + + def verify(self, digest, r, s): + """ + Verify a newly calculated digest against the signature + values r and s. + + @type digest: str + @param digest: SHA-1 hash of message (same as output + from MessageDigest, a "byte string") + @type r: str + @param r: r value of the signature, a "byte string" + @type s: str + @param s: s value of the signature, a "byte string" + @rtype: int + @return: 1 (true) if verify succeeded, 0 if failed + """ + assert self.check_key(), 'key is not initialised' + return m2.dsa_verify(self.dsa, digest, r, s) + + def sign_asn1(self, digest): + assert self.check_key(), 'key is not initialised' + return m2.dsa_sign_asn1(self.dsa, digest) + + def verify_asn1(self, digest, blob): + assert self.check_key(), 'key is not initialised' + return m2.dsa_verify_asn1(self.dsa, digest, blob) + + def check_key(self): + """ + Check to be sure the DSA object has a valid private key. + + @rtype: int + @return: 1 (true) if a valid private key + """ + assert m2.dsa_type_check(self.dsa), "'dsa' type error" + return m2.dsa_check_key(self.dsa) + + + +class DSA_pub(DSA): + + """ + This class is a DSA context that only supports a public key + and verification. It does NOT support a private key or + signing. + + """ + + def sign(self, *argv): + raise DSAError('DSA_pub object has no private key') + + sign_asn1 = sign + + def check_key(self): + return m2.dsa_check_pub_key(self.dsa) + + save_key = DSA.save_pub_key + + save_key_bio = DSA.save_pub_key_bio + +#--------------------------------------------------------------- +# factories and other functions + +def gen_params(bits, callback=util.genparam_callback): + """ + Factory function that generates DSA parameters and + instantiates a DSA object from the output. + + @type bits: int + @param bits: The length of the prime to be generated. If + 'bits' < 512, it is set to 512. + @type callback: function + @param callback: A Python callback object that will be + invoked during parameter generation; it usual + purpose is to provide visual feedback. + @rtype: DSA + @return: instance of DSA. + """ + dsa = m2.dsa_generate_parameters(bits, callback) + if dsa is None: + raise DSAError('problem generating DSA parameters') + return DSA(dsa, 1) + +def set_params(p, q, g): + """ + Factory function that instantiates a DSA object with DSA + parameters. + + @type p: str + @param p: value of p, a "byte string" + @type q: str + @param q: value of q, a "byte string" + @type g: str + @param g: value of g, a "byte string" + @rtype: DSA + @return: instance of DSA. + """ + dsa = m2.dsa_new() + m2.dsa_set_p(dsa, p) + m2.dsa_set_q(dsa, q) + m2.dsa_set_g(dsa, g) + return DSA(dsa, 1) + +def load_params(file, callback=util.passphrase_callback): + """ + Factory function that instantiates a DSA object with DSA + parameters from a file. + + @type file: str + @param file: Names the file (a path) that contains the PEM + representation of the DSA parameters. + @type callback: A Python callable + @param callback: A Python callback object that will be + invoked if the DSA parameters file is + passphrase-protected. + @rtype: DSA + @return: instance of DSA. + """ + bio = BIO.openfile(file) + ret = load_params_bio(bio, callback) + bio.close() + return ret + + +def load_params_bio(bio, callback=util.passphrase_callback): + """ + Factory function that instantiates a DSA object with DSA + parameters from a M2Crypto.BIO object. + + @type bio: M2Crypto.BIO object + @param bio: Contains the PEM representation of the DSA + parameters. + @type callback: A Python callable + @param callback: A Python callback object that will be + invoked if the DSA parameters file is + passphrase-protected. + @rtype: DSA + @return: instance of DSA. + """ + dsa = m2.dsa_read_params(bio._ptr(), callback) + if dsa is None: + raise DSAError('problem loading DSA parameters') + return DSA(dsa, 1) + + +def load_key(file, callback=util.passphrase_callback): + """ + Factory function that instantiates a DSA object from a + PEM encoded DSA key pair. + + @type file: str + @param file: Names the file (a path) that contains the PEM + representation of the DSA key pair. + @type callback: A Python callable + @param callback: A Python callback object that will be + invoked if the DSA key pair is + passphrase-protected. + @rtype: DSA + @return: instance of DSA. + """ + bio = BIO.openfile(file) + ret = load_key_bio(bio, callback) + bio.close() + return ret + + +def load_key_bio(bio, callback=util.passphrase_callback): + """ + Factory function that instantiates a DSA object from a + PEM encoded DSA key pair. + + @type bio: M2Crypto.BIO object + @param bio: Contains the PEM representation of the DSA + key pair. + @type callback: A Python callable + @param callback: A Python callback object that will be + invoked if the DSA key pair is + passphrase-protected. + @rtype: DSA + @return: instance of DSA. + """ + dsa = m2.dsa_read_key(bio._ptr(), callback) + if not dsa: + raise DSAError('problem loading DSA key pair') + return DSA(dsa, 1) + + +def load_pub_key(file, callback=util.passphrase_callback): + """ + Factory function that instantiates a DSA_pub object using + a DSA public key contained in PEM file. The PEM file + must contain the parameters in addition to the public key. + + @type file: str + @param file: Names the file (a path) that contains the PEM + representation of the DSA public key. + @type callback: A Python callable + @param callback: A Python callback object that will be + invoked should the DSA public key be + passphrase-protected. + @rtype: DSA_pub + @return: instance of DSA_pub. + """ + bio = BIO.openfile(file) + ret = load_pub_key_bio(bio, callback) + bio.close() + return ret + + +def load_pub_key_bio(bio, callback=util.passphrase_callback): + """ + Factory function that instantiates a DSA_pub object using + a DSA public key contained in PEM format. The PEM + must contain the parameters in addition to the public key. + + @type bio: M2Crypto.BIO object + @param bio: Contains the PEM representation of the DSA + public key (with params). + @type callback: A Python callable + @param callback: A Python callback object that will be + invoked should the DSA public key be + passphrase-protected. + @rtype: DSA_pub + @return: instance of DSA_pub. + """ + dsapub = m2.dsa_read_pub_key(bio._ptr(), callback) + if not dsapub: + raise DSAError('problem loading DSA public key') + return DSA_pub(dsapub, 1) diff --git a/M2Crypto/EC.py b/M2Crypto/EC.py new file mode 100644 index 0000000..b1ed43e --- /dev/null +++ b/M2Crypto/EC.py @@ -0,0 +1,335 @@ +""" +M2Crypto wrapper for OpenSSL ECDH/ECDSA API. + +@requires: OpenSSL 0.9.8 or newer + +Copyright (c) 1999-2003 Ng Pheng Siong. All rights reserved. + +Portions copyright (c) 2005-2006 Vrije Universiteit Amsterdam. +All rights reserved.""" + +import util, BIO, m2 + +class ECError(Exception): pass + +m2.ec_init(ECError) + +# Curve identifier constants +NID_secp112r1 = m2.NID_secp112r1 +NID_secp112r2 = m2.NID_secp112r2 +NID_secp128r1 = m2.NID_secp128r1 +NID_secp128r2 = m2.NID_secp128r2 +NID_secp160k1 = m2.NID_secp160k1 +NID_secp160r1 = m2.NID_secp160r1 +NID_secp160r2 = m2.NID_secp160r2 +NID_secp192k1 = m2.NID_secp192k1 +NID_secp224k1 = m2.NID_secp224k1 +NID_secp224r1 = m2.NID_secp224r1 +NID_secp256k1 = m2.NID_secp256k1 +NID_secp384r1 = m2.NID_secp384r1 +NID_secp521r1 = m2.NID_secp521r1 +NID_sect113r1 = m2.NID_sect113r1 +NID_sect113r2 = m2.NID_sect113r2 +NID_sect131r1 = m2.NID_sect131r1 +NID_sect131r2 = m2.NID_sect131r2 +NID_sect163k1 = m2.NID_sect163k1 +NID_sect163r1 = m2.NID_sect163r1 +NID_sect163r2 = m2.NID_sect163r2 +NID_sect193r1 = m2.NID_sect193r1 +NID_sect193r2 = m2.NID_sect193r2 +NID_sect233k1 = m2.NID_sect233k1 # default for secg.org TLS test server +NID_sect233r1 = m2.NID_sect233r1 +NID_sect239k1 = m2.NID_sect239k1 +NID_sect283k1 = m2.NID_sect283k1 +NID_sect283r1 = m2.NID_sect283r1 +NID_sect409k1 = m2.NID_sect409k1 +NID_sect409r1 = m2.NID_sect409r1 +NID_sect571k1 = m2.NID_sect571k1 +NID_sect571r1 = m2.NID_sect571r1 + +NID_X9_62_prime192v1 = m2.NID_X9_62_prime192v1 +NID_X9_62_prime192v2 = m2.NID_X9_62_prime192v2 +NID_X9_62_prime192v3 = m2.NID_X9_62_prime192v3 +NID_X9_62_prime239v1 = m2.NID_X9_62_prime239v1 +NID_X9_62_prime239v2 = m2.NID_X9_62_prime239v2 +NID_X9_62_prime239v3 = m2.NID_X9_62_prime239v3 +NID_X9_62_prime256v1 = m2.NID_X9_62_prime256v1 +NID_X9_62_c2pnb163v1 = m2.NID_X9_62_c2pnb163v1 +NID_X9_62_c2pnb163v2 = m2.NID_X9_62_c2pnb163v2 +NID_X9_62_c2pnb163v3 = m2.NID_X9_62_c2pnb163v3 +NID_X9_62_c2pnb176v1 = m2.NID_X9_62_c2pnb176v1 +NID_X9_62_c2tnb191v1 = m2.NID_X9_62_c2tnb191v1 +NID_X9_62_c2tnb191v2 = m2.NID_X9_62_c2tnb191v2 +NID_X9_62_c2tnb191v3 = m2.NID_X9_62_c2tnb191v3 +NID_X9_62_c2pnb208w1 = m2.NID_X9_62_c2pnb208w1 +NID_X9_62_c2tnb239v1 = m2.NID_X9_62_c2tnb239v1 +NID_X9_62_c2tnb239v2 = m2.NID_X9_62_c2tnb239v2 +NID_X9_62_c2tnb239v3 = m2.NID_X9_62_c2tnb239v3 +NID_X9_62_c2pnb272w1 = m2.NID_X9_62_c2pnb272w1 +NID_X9_62_c2pnb304w1 = m2.NID_X9_62_c2pnb304w1 +NID_X9_62_c2tnb359v1 = m2.NID_X9_62_c2tnb359v1 +NID_X9_62_c2pnb368w1 = m2.NID_X9_62_c2pnb368w1 +NID_X9_62_c2tnb431r1 = m2.NID_X9_62_c2tnb431r1 + +NID_wap_wsg_idm_ecid_wtls1 = m2.NID_wap_wsg_idm_ecid_wtls1 +NID_wap_wsg_idm_ecid_wtls3 = m2.NID_wap_wsg_idm_ecid_wtls3 +NID_wap_wsg_idm_ecid_wtls4 = m2.NID_wap_wsg_idm_ecid_wtls4 +NID_wap_wsg_idm_ecid_wtls5 = m2.NID_wap_wsg_idm_ecid_wtls5 +NID_wap_wsg_idm_ecid_wtls6 = m2.NID_wap_wsg_idm_ecid_wtls6 +NID_wap_wsg_idm_ecid_wtls7 = m2.NID_wap_wsg_idm_ecid_wtls7 +NID_wap_wsg_idm_ecid_wtls8 = m2.NID_wap_wsg_idm_ecid_wtls8 +NID_wap_wsg_idm_ecid_wtls9 = m2.NID_wap_wsg_idm_ecid_wtls9 +NID_wap_wsg_idm_ecid_wtls10 = m2.NID_wap_wsg_idm_ecid_wtls10 +NID_wap_wsg_idm_ecid_wtls11 = m2.NID_wap_wsg_idm_ecid_wtls11 +NID_wap_wsg_idm_ecid_wtls12 = m2.NID_wap_wsg_idm_ecid_wtls12 + +# The following two curves, according to OpenSSL, have a +# "Questionable extension field!" and are not supported by +# the OpenSSL inverse function. ECError: no inverse. +# As such they cannot be used for signing. They might, +# however, be usable for encryption but that has not +# been tested. Until thir usefulness can be established, +# they are not supported at this time. +# NID_ipsec3 = m2.NID_ipsec3 +# NID_ipsec4 = m2.NID_ipsec4 + + +class EC: + + """ + Object interface to a EC key pair. + """ + + m2_ec_key_free = m2.ec_key_free + + def __init__(self, ec, _pyfree=0): + assert m2.ec_key_type_check(ec), "'ec' type error" + self.ec = ec + self._pyfree = _pyfree + + def __del__(self): + if getattr(self, '_pyfree', 0): + self.m2_ec_key_free(self.ec) + + def __len__(self): + assert m2.ec_key_type_check(self.ec), "'ec' type error" + return m2.ec_key_keylen(self.ec) + + def gen_key(self): + """ + Generates the key pair from its parameters. Use:: + keypair = EC.gen_params(curve) + keypair.gen_key() + to create an EC key pair. + """ + assert m2.ec_key_type_check(self.ec), "'ec' type error" + m2.ec_key_gen_key(self.ec) + + def pub(self): + # Don't let python free + return EC_pub(self.ec, 0) + + def sign_dsa(self, digest): + """ + Sign the given digest using ECDSA. Returns a tuple (r,s), the two + ECDSA signature parameters. + """ + assert self._check_key_type(), "'ec' type error" + return m2.ecdsa_sign(self.ec, digest) + + def verify_dsa(self, digest, r, s): + """ + Verify the given digest using ECDSA. r and s are the ECDSA + signature parameters. + """ + assert self._check_key_type(), "'ec' type error" + return m2.ecdsa_verify(self.ec, digest, r, s) + + def sign_dsa_asn1(self, digest): + assert self._check_key_type(), "'ec' type error" + return m2.ecdsa_sign_asn1(self.ec, digest) + + def verify_dsa_asn1(self, digest, blob): + assert self._check_key_type(), "'ec' type error" + return m2.ecdsa_verify_asn1(self.ec, digest, blob) + + def compute_dh_key(self,pub_key): + """ + Compute the ECDH shared key of this key pair and the given public + key object. They must both use the same curve. Returns the + shared key in binary as a buffer object. No Key Derivation Function is + applied. + """ + assert self.check_key(), 'key is not initialised' + return m2.ecdh_compute_key(self.ec, pub_key.ec) + + def save_key_bio(self, bio, cipher='aes_128_cbc', callback=util.passphrase_callback): + """ + Save the key pair to an M2Crypto.BIO.BIO object in PEM format. + + @type bio: M2Crypto.BIO.BIO + @param bio: M2Crypto.BIO.BIO object to save key to. + + @type cipher: string + @param cipher: Symmetric cipher to protect the key. The default + cipher is 'aes_128_cbc'. If cipher is None, then the key is saved + in the clear. + + @type callback: Python callable + @param callback: A Python callable object that is invoked + to acquire a passphrase with which to protect the key. + The default is util.passphrase_callback. + """ + if cipher is None: + return m2.ec_key_write_bio_no_cipher(self.ec, bio._ptr(), callback) + else: + ciph = getattr(m2, cipher, None) + if ciph is None: + raise ValueError('not such cipher %s' % cipher) + return m2.ec_key_write_bio(self.ec, bio._ptr(), ciph(), callback) + + def save_key(self, file, cipher='aes_128_cbc', callback=util.passphrase_callback): + """ + Save the key pair to a file in PEM format. + + @type file: string + @param file: Name of file to save key to. + + @type cipher: string + @param cipher: Symmetric cipher to protect the key. The default + cipher is 'aes_128_cbc'. If cipher is None, then the key is saved + in the clear. + + @type callback: Python callable + @param callback: A Python callable object that is invoked + to acquire a passphrase with which to protect the key. + The default is util.passphrase_callback. + """ + bio = BIO.openfile(file, 'wb') + return self.save_key_bio(bio, cipher, callback) + + def save_pub_key_bio(self, bio): + """ + Save the public key to an M2Crypto.BIO.BIO object in PEM format. + + @type bio: M2Crypto.BIO.BIO + @param bio: M2Crypto.BIO.BIO object to save key to. + """ + return m2.ec_key_write_pubkey(self.ec, bio._ptr()) + + def save_pub_key(self, file): + """ + Save the public key to a file in PEM format. + + @type file: string + @param file: Name of file to save key to. + """ + bio = BIO.openfile(file, 'wb') + return m2.ec_key_write_pubkey(self.ec, bio._ptr()) + + def _check_key_type(self): + return m2.ec_key_type_check(self.ec) + + def check_key(self): + assert m2.ec_key_type_check(self.ec), "'ec' type error" + return m2.ec_key_check_key(self.ec) + + +class EC_pub(EC): + + """ + Object interface to an EC public key. + ((don't like this implementation inheritance)) + """ + def __init__(self,ec,_pyfree=0): + EC.__init__(self,ec,_pyfree) + self.der = None + + def get_der(self): + """ + Returns the public key in DER format as a buffer object. + """ + assert self.check_key(), 'key is not initialised' + if self.der is None: + self.der = m2.ec_key_get_public_der(self.ec) + return self.der + + save_key = EC.save_pub_key + + save_key_bio = EC.save_pub_key_bio + + +def gen_params(curve): + """ + Factory function that generates EC parameters and + instantiates a EC object from the output. + + @param curve: This is the OpenSSL nid of the curve to use. + """ + return EC(m2.ec_key_new_by_curve_name(curve), 1) + + +def load_key(file, callback=util.passphrase_callback): + """ + Factory function that instantiates a EC object. + + @param file: Names the file that contains the PEM representation + of the EC key pair. + + @param callback: Python callback object that will be invoked + if the EC key pair is passphrase-protected. + """ + bio = BIO.openfile(file) + return load_key_bio(bio, callback) + + +def load_key_bio(bio, callback=util.passphrase_callback): + """ + Factory function that instantiates a EC object. + + @param bio: M2Crypto.BIO object that contains the PEM + representation of the EC key pair. + + @param callback: Python callback object that will be invoked + if the EC key pair is passphrase-protected. + """ + return EC(m2.ec_key_read_bio(bio._ptr(), callback), 1) + +def load_pub_key(file): + """ + Load an EC public key from file. + + @type file: string + @param file: Name of file containing EC public key in PEM format. + + @rtype: M2Crypto.EC.EC_pub + @return: M2Crypto.EC.EC_pub object. + """ + bio = BIO.openfile(file) + return load_pub_key_bio(bio) + + +def load_pub_key_bio(bio): + """ + Load an EC public key from an M2Crypto.BIO.BIO object. + + @type bio: M2Crypto.BIO.BIO + @param bio: M2Crypto.BIO.BIO object containing EC public key in PEM + format. + + @rtype: M2Crypto.EC.EC_pub + @return: M2Crypto.EC.EC_pub object. + """ + ec = m2.ec_key_read_pubkey(bio._ptr()) + if ec is None: + ec_error() + return EC_pub(ec, 1) + +def ec_error(): + raise ECError, m2.err_reason_error_string(m2.err_get_error()) + +def pub_key_from_der(der): + """ + Create EC_pub from DER. + """ + return EC_pub(m2.ec_key_from_pubkey_der(der), 1) diff --git a/M2Crypto/EVP.py b/M2Crypto/EVP.py new file mode 100644 index 0000000..cb92380 --- /dev/null +++ b/M2Crypto/EVP.py @@ -0,0 +1,408 @@ +"""M2Crypto wrapper for OpenSSL EVP API. + +Copyright (c) 1999-2004 Ng Pheng Siong. All rights reserved. + +Portions Copyright (c) 2004-2007 Open Source Applications Foundation. +Author: Heikki Toivonen +""" + +from M2Crypto import Err, util, BIO, RSA +import m2 + +class EVPError(Exception): pass + +m2.evp_init(EVPError) + + +def pbkdf2(password, salt, iter, keylen): + """ + Derive a key from password using PBKDF2 algorithm specified in RFC 2898. + + @param password: Derive the key from this password. + @type password: str + @param salt: Salt. + @type salt: str + @param iter: Number of iterations to perform. + @type iter: int + @param keylen: Length of key to produce. + @type keylen: int + @return: Key. + @rtype: str + """ + return m2.pkcs5_pbkdf2_hmac_sha1(password, salt, iter, keylen) + +class MessageDigest: + """ + Message Digest + """ + m2_md_ctx_free = m2.md_ctx_free + + def __init__(self, algo): + md = getattr(m2, algo, None) + if md is None: + raise ValueError, ('unknown algorithm', algo) + self.md=md() + self.ctx=m2.md_ctx_new() + m2.digest_init(self.ctx, self.md) + + def __del__(self): + if getattr(self, 'ctx', None): + self.m2_md_ctx_free(self.ctx) + + def update(self, data): + """ + Add data to be digested. + + @return: -1 for Python error, 1 for success, 0 for OpenSSL failure. + """ + return m2.digest_update(self.ctx, data) + + def final(self): + return m2.digest_final(self.ctx) + + # Deprecated. + digest = final + + +class HMAC: + + m2_hmac_ctx_free = m2.hmac_ctx_free + + def __init__(self, key, algo='sha1'): + md = getattr(m2, algo, None) + if md is None: + raise ValueError, ('unknown algorithm', algo) + self.md=md() + self.ctx=m2.hmac_ctx_new() + m2.hmac_init(self.ctx, key, self.md) + + def __del__(self): + if getattr(self, 'ctx', None): + self.m2_hmac_ctx_free(self.ctx) + + def reset(self, key): + m2.hmac_init(self.ctx, key, self.md) + + def update(self, data): + m2.hmac_update(self.ctx, data) + + def final(self): + return m2.hmac_final(self.ctx) + + digest=final + +def hmac(key, data, algo='sha1'): + md = getattr(m2, algo, None) + if md is None: + raise ValueError, ('unknown algorithm', algo) + return m2.hmac(key, data, md()) + + +class Cipher: + + m2_cipher_ctx_free = m2.cipher_ctx_free + + def __init__(self, alg, key, iv, op, key_as_bytes=0, d='md5', salt='12345678', i=1, padding=1): + cipher = getattr(m2, alg, None) + if cipher is None: + raise ValueError, ('unknown cipher', alg) + self.cipher=cipher() + if key_as_bytes: + kmd = getattr(m2, d, None) + if kmd is None: + raise ValueError, ('unknown message digest', d) + key = m2.bytes_to_key(self.cipher, kmd(), key, salt, iv, i) + self.ctx=m2.cipher_ctx_new() + m2.cipher_init(self.ctx, self.cipher, key, iv, op) + self.set_padding(padding) + del key + + def __del__(self): + if getattr(self, 'ctx', None): + self.m2_cipher_ctx_free(self.ctx) + + def update(self, data): + return m2.cipher_update(self.ctx, data) + + def final(self): + return m2.cipher_final(self.ctx) + + def set_padding(self, padding=1): + return m2.cipher_set_padding(self.ctx, padding) + + +class PKey: + """ + Public Key + """ + + m2_pkey_free = m2.pkey_free + m2_md_ctx_free = m2.md_ctx_free + + def __init__(self, pkey=None, _pyfree=0, md='sha1'): + if pkey is not None: + self.pkey = pkey + self._pyfree = _pyfree + else: + self.pkey = m2.pkey_new() + self._pyfree = 1 + self._set_context(md) + + def __del__(self): + if getattr(self, '_pyfree', 0): + self.m2_pkey_free(self.pkey) + if getattr(self, 'ctx', None): + self.m2_md_ctx_free(self.ctx) + + def _ptr(self): + return self.pkey + + def _set_context(self, md): + mda = getattr(m2, md, None) + if mda is None: + raise ValueError, ('unknown message digest', md) + self.md = mda() + self.ctx = m2.md_ctx_new() + + def reset_context(self, md='sha1'): + """ + Reset internal message digest context. + + @type md: string + @param md: The message digest algorithm. + """ + self._set_context(md) + + def sign_init(self): + """ + Initialise signing operation with self. + """ + m2.sign_init(self.ctx, self.md) + + def sign_update(self, data): + """ + Feed data to signing operation. + + @type data: string + @param data: Data to be signed. + """ + m2.sign_update(self.ctx, data) + + def sign_final(self): + """ + Return signature. + + @rtype: string + @return: The signature. + """ + return m2.sign_final(self.ctx, self.pkey) + + # Deprecated + update = sign_update + final = sign_final + + def verify_init(self): + """ + Initialise signature verification operation with self. + """ + m2.verify_init(self.ctx, self.md) + + def verify_update(self, data): + """ + Feed data to verification operation. + + @type data: string + @param data: Data to be verified. + @return: -1 on Python error, 1 for success, 0 for OpenSSL error + """ + return m2.verify_update(self.ctx, data) + + def verify_final(self, sign): + """ + Return result of verification. + + @param sign: Signature to use for verification + @rtype: int + @return: Result of verification: 1 for success, 0 for failure, -1 on + other error. + """ + return m2.verify_final(self.ctx, sign, self.pkey) + + def assign_rsa(self, rsa, capture=1): + """ + Assign the RSA key pair to self. + + @type rsa: M2Crypto.RSA.RSA + @param rsa: M2Crypto.RSA.RSA object to be assigned to self. + + @type capture: boolean + @param capture: If true (default), this PKey object will own the RSA + object, meaning that once the PKey object gets + deleted it is no longer safe to use the RSA object. + + @rtype: int + @return: Return 1 for success and 0 for failure. + """ + if capture: + ret = m2.pkey_assign_rsa(self.pkey, rsa.rsa) + if ret: + rsa._pyfree = 0 + else: + ret = m2.pkey_set1_rsa(self.pkey, rsa.rsa) + return ret + + def get_rsa(self): + """ + Return the underlying RSA key if that is what the EVP + instance is holding. + """ + rsa_ptr = m2.pkey_get1_rsa(self.pkey) + if rsa_ptr is None: + raise ValueError("PKey instance is not holding a RSA key") + + rsa = RSA.RSA_pub(rsa_ptr, 1) + return rsa + + def save_key(self, file, cipher='aes_128_cbc', callback=util.passphrase_callback): + """ + Save the key pair to a file in PEM format. + + @type file: string + @param file: Name of file to save key to. + + @type cipher: string + @param cipher: Symmetric cipher to protect the key. The default + cipher is 'aes_128_cbc'. If cipher is None, then the key is saved + in the clear. + + @type callback: Python callable + @param callback: A Python callable object that is invoked + to acquire a passphrase with which to protect the key. + The default is util.passphrase_callback. + """ + bio = BIO.openfile(file, 'wb') + return self.save_key_bio(bio, cipher, callback) + + def save_key_bio(self, bio, cipher='aes_128_cbc', callback=util.passphrase_callback): + """ + Save the key pair to the M2Crypto.BIO object 'bio' in PEM format. + + @type bio: M2Crypto.BIO + @param bio: M2Crypto.BIO object to save key to. + + @type cipher: string + @param cipher: Symmetric cipher to protect the key. The default + cipher is 'aes_128_cbc'. If cipher is None, then the key is saved + in the clear. + + @type callback: Python callable + @param callback: A Python callable object that is invoked + to acquire a passphrase with which to protect the key. + The default is util.passphrase_callback. + """ + if cipher is None: + return m2.pkey_write_pem_no_cipher(self.pkey, bio._ptr(), callback) + else: + proto = getattr(m2, cipher, None) + if proto is None: + raise ValueError, 'no such cipher %s' % cipher + return m2.pkey_write_pem(self.pkey, bio._ptr(), proto(), callback) + + def as_pem(self, cipher='aes_128_cbc', callback=util.passphrase_callback): + """ + Return key in PEM format in a string. + + @type cipher: string + @param cipher: Symmetric cipher to protect the key. The default + cipher is 'aes_128_cbc'. If cipher is None, then the key is saved + in the clear. + + @type callback: Python callable + @param callback: A Python callable object that is invoked + to acquire a passphrase with which to protect the key. + The default is util.passphrase_callback. + """ + bio = BIO.MemoryBuffer() + self.save_key_bio(bio, cipher, callback) + return bio.read_all() + + def as_der(self): + """ + Return key in DER format in a string + """ + buf = m2.pkey_as_der(self.pkey) + bio = BIO.MemoryBuffer(buf) + return bio.read_all() + + def size(self): + """ + Return the size of the key in bytes. + """ + return m2.pkey_size(self.pkey) + + def get_modulus(self): + """ + Return the modulus in hex format. + """ + return m2.pkey_get_modulus(self.pkey) + + +def load_key(file, callback=util.passphrase_callback): + """ + Load an M2Crypto.EVP.PKey from file. + + @type file: string + @param file: Name of file containing the key in PEM format. + + @type callback: Python callable + @param callback: A Python callable object that is invoked + to acquire a passphrase with which to protect the key. + + @rtype: M2Crypto.EVP.PKey + @return: M2Crypto.EVP.PKey object. + """ + bio = m2.bio_new_file(file, 'r') + if bio is None: + raise BIO.BIOError(Err.get_error()) + cptr = m2.pkey_read_pem(bio, callback) + m2.bio_free(bio) + if cptr is None: + raise EVPError(Err.get_error()) + return PKey(cptr, 1) + +def load_key_bio(bio, callback=util.passphrase_callback): + """ + Load an M2Crypto.EVP.PKey from an M2Crypto.BIO object. + + @type bio: M2Crypto.BIO + @param bio: M2Crypto.BIO object containing the key in PEM format. + + @type callback: Python callable + @param callback: A Python callable object that is invoked + to acquire a passphrase with which to protect the key. + + @rtype: M2Crypto.EVP.PKey + @return: M2Crypto.EVP.PKey object. + """ + cptr = m2.pkey_read_pem(bio._ptr(), callback) + if cptr is None: + raise EVPError(Err.get_error()) + return PKey(cptr, 1) + +def load_key_string(string, callback=util.passphrase_callback): + """ + Load an M2Crypto.EVP.PKey from a string. + + @type string: string + @param string: String containing the key in PEM format. + + @type callback: Python callable + @param callback: A Python callable object that is invoked + to acquire a passphrase with which to protect the key. + + @rtype: M2Crypto.EVP.PKey + @return: M2Crypto.EVP.PKey object. + """ + bio = BIO.MemoryBuffer(string) + return load_key_bio( bio, callback) + diff --git a/M2Crypto/Engine.py b/M2Crypto/Engine.py new file mode 100644 index 0000000..d6b879b --- /dev/null +++ b/M2Crypto/Engine.py @@ -0,0 +1,119 @@ +# vim: sts=4 sw=4 et +""" +M2Crypto wrapper for OpenSSL ENGINE API. + +Pavel Shramov +IMEC MSU +""" + +from M2Crypto import m2, EVP, X509, Err + +class EngineError(Exception): pass + +m2.engine_init_error(EngineError) + +class Engine: + """Wrapper for ENGINE object.""" + + m2_engine_free = m2.engine_free + + def __init__(self, id = None, _ptr = None, _pyfree = 1): + """Create new Engine from ENGINE pointer or obtain by id""" + if not _ptr and not id: + raise ValueError("No engine id specified") + self._ptr = _ptr + if not self._ptr: + self._ptr = m2.engine_by_id(id) + if not self._ptr: + raise ValueError("Unknown engine: %s" % id) + self._pyfree = _pyfree + + def __del__(self): + if getattr(self, '_pyfree', 0): + self.m2_engine_free(self._ptr) + + def init(self): + """Obtain a functional reference to the engine. + + @return: 0 on error, non-zero on success.""" + return m2.engine_init(self._ptr) + + def finish(self): + """Release a functional and structural reference to the engine.""" + return m2.engine_finish(self._ptr) + + def ctrl_cmd_string(self, cmd, arg, optional = 0): + """Call ENGINE_ctrl_cmd_string""" + if not m2.engine_ctrl_cmd_string(self._ptr, cmd, arg, optional): + raise EngineError(Err.get_error()) + + def get_name(self): + """Return engine name""" + return m2.engine_get_name(self._ptr) + + def get_id(self): + """Return engine id""" + return m2.engine_get_id(self._ptr) + + def set_default(self, methods = m2.ENGINE_METHOD_ALL): + """Use this engine as default for methods specified in argument + Possible values are bitwise OR of m2.ENGINE_METHOD_*""" + return m2.engine_set_default(self._ptr, methods) + + def _engine_load_key(self, func, name, pin = None): + """Helper function for loading keys""" + ui = m2.ui_openssl() + cbd = m2.engine_pkcs11_data_new(pin) + try: + kptr = func(self._ptr, name, ui, cbd) + if not kptr: + raise EngineError(Err.get_error()) + key = EVP.PKey(kptr, _pyfree = 1) + finally: + m2.engine_pkcs11_data_free(cbd) + return key + + def load_private_key(self, name, pin = None): + """Load private key with engine methods (e.g from smartcard). + If pin is not set it will be asked + """ + return self._engine_load_key(m2.engine_load_private_key, name, pin) + + def load_public_key(self, name, pin = None): + """Load public key with engine methods (e.g from smartcard).""" + return self._engine_load_key(m2.engine_load_public_key, name, pin) + + def load_certificate(self, name): + """Load certificate from engine (e.g from smartcard). + NOTE: This function may be not implemented by engine!""" + cptr = m2.engine_load_certificate(self._ptr, name) + if not cptr: + raise EngineError("Certificate or card not found") + return X509.X509(cptr, _pyfree = 1) + + +def load_dynamic_engine(id, sopath): + """Load and return dymanic engine from sopath and assign id to it""" + m2.engine_load_dynamic() + e = Engine('dynamic') + e.ctrl_cmd_string("SO_PATH", sopath) + e.ctrl_cmd_string("ID", id) + e.ctrl_cmd_string("LIST_ADD", "1") + e.ctrl_cmd_string("LOAD", None) + return e + + +def load_dynamic(): + """Load dynamic engine""" + m2.engine_load_dynamic() + + +def load_openssl(): + """Load openssl engine""" + m2.engine_load_openssl() + + +def cleanup(): + """If you load any engines, you need to clean up after your application + is finished with the engines.""" + m2.engine_cleanup() diff --git a/M2Crypto/Err.py b/M2Crypto/Err.py new file mode 100644 index 0000000..3588f76 --- /dev/null +++ b/M2Crypto/Err.py @@ -0,0 +1,49 @@ +"""M2Crypto wrapper for OpenSSL Error API. + +Copyright (c) 1999-2003 Ng Pheng Siong. All rights reserved.""" + +import BIO +import m2 + +def get_error(): + err=BIO.MemoryBuffer() + m2.err_print_errors(err.bio_ptr()) + return err.getvalue() + +def get_error_code(): + return m2.err_get_error() + +def peek_error_code(): + return m2.err_peek_error() + +def get_error_lib(err): + return m2.err_lib_error_string(err) + +def get_error_func(err): + return m2.err_func_error_string(err) + +def get_error_reason(err): + return m2.err_reason_error_string(err) + +def get_x509_verify_error(err): + return m2.x509_get_verify_error(err) + +class SSLError(Exception): + def __init__(self, err, client_addr): + self.err = err + self.client_addr = client_addr + + def __str__(self): + if (isinstance(self.client_addr, unicode)): + s = self.client_addr.encode('utf8') + else: + s = self.client_addr + return "%s: %s: %s" % \ + (m2.err_func_error_string(self.err), \ + s, \ + m2.err_reason_error_string(self.err)) + +class M2CryptoError(Exception): + pass + + diff --git a/M2Crypto/PGP/PublicKey.py b/M2Crypto/PGP/PublicKey.py new file mode 100644 index 0000000..f892ade --- /dev/null +++ b/M2Crypto/PGP/PublicKey.py @@ -0,0 +1,58 @@ +"""M2Crypto PGP2. + +Copyright (c) 1999-2003 Ng Pheng Siong. All rights reserved.""" + +from constants import * +from packet import * +import RSA + +class PublicKey: + def __init__(self, pubkey_pkt): + import warnings + warnings.warn('Deprecated. No maintainer for PGP. If you use this, please inform M2Crypto maintainer.', DeprecationWarning) + + self._pubkey_pkt = pubkey_pkt + self._pubkey = RSA.new_pub_key((pubkey_pkt._e, pubkey_pkt._n)) + self._userid = {} + self._signature = {} + + def keyid(self): + return self._pubkey.n[-8:] + + def add_userid(self, u_pkt): + assert isinstance(u_pkt, userid_packet) + self._userid[u_pkt.userid()] = u_pkt + + def remove_userid(self, userid): + del self._userid[userid] + + def add_signature(self, userid, s_pkt): + assert isinstance(s_pkt, signature_packet) + assert self._userid.has_key(userid) + if self._signature.has_key(userid): + self._signature.append(s_pkt) + else: + self._signature = [s_pkt] + + def __getitem__(self, id): + return self._userid[id] + + def __setitem__(self, *args): + raise NotImplementedError + + def __delitem__(self, id): + del self._userid[id] + if self._signature[id]: + del self._signature[id] + + def write(self, stream): + pass + + def encrypt(self, ptxt): + # XXX Munge ptxt into pgp format. + return self._pubkey.public_encrypt(ptxt, RSA.pkcs1_padding) + + def decrypt(self, ctxt): + # XXX Munge ctxt into pgp format. + return self._pubkey.public_encrypt(ctxt, RSA.pkcs1_padding) + diff --git a/M2Crypto/PGP/PublicKeyRing.py b/M2Crypto/PGP/PublicKeyRing.py new file mode 100644 index 0000000..a8447b3 --- /dev/null +++ b/M2Crypto/PGP/PublicKeyRing.py @@ -0,0 +1,81 @@ +"""M2Crypto PGP2. + +Copyright (c) 1999-2003 Ng Pheng Siong. All rights reserved.""" + +from constants import * +from packet import * +from PublicKey import * + +class PublicKeyRing: + def __init__(self, keyring): + import warnings + warnings.warn('Deprecated. No maintainer for PGP. If you use this, please inform M2Crypto maintainer.', DeprecationWarning) + + self._keyring = keyring + self._userid = {} + self._keyid = {} + self._spurious = [] + self._pubkey = [] + + def load(self): + curr_pub = None + curr_index = -1 + + ps = packet_stream(self._keyring) + while 1: + pkt = ps.read() + + if pkt is None: + break + + elif isinstance(pkt, public_key_packet): + curr_index = curr_index + 1 + curr_pub = PublicKey(pkt) + self._pubkey.append(curr_pub) + #self._keyid[curr_pub.keyid()] = (curr_pub, curr_index) + + elif isinstance(pkt, userid_packet): + if curr_pub is None: + self._spurious.append(pkt) + else: + curr_pub.add_userid(pkt) + self._userid[pkt.userid()] = (curr_pub, curr_index) + + elif isinstance(pkt, signature_packet): + if curr_pub is None: + self._spurious.append(pkt) + else: + curr_pub.add_signature(pkt) + + else: + self._spurious.append(pkt) + + ps.close() + + def __getitem__(self, id): + return self._userid[id][0] + + def __setitem__(self, *args): + raise NotImplementedError + + def __delitem__(self, id): + pkt, idx = self._userid[id] + del self._pubkey[idx] + del self._userid[idx] + pkt, idx = self._keyid[id] + del self._keyid[idx] + + def spurious(self): + return tuple(self._spurious) + + def save(self, keyring): + for p in self._pubkey: + pp = p.pack() + keyring.write(pp) + + +def load_pubring(filename='pubring.pgp'): + pkr = PublicKeyRing(open(filename, 'rb')) + pkr.load() + return pkr + diff --git a/M2Crypto/PGP/RSA.py b/M2Crypto/PGP/RSA.py new file mode 100644 index 0000000..153193d --- /dev/null +++ b/M2Crypto/PGP/RSA.py @@ -0,0 +1,36 @@ +"""M2Crypto PGP2 RSA. + +Copyright (c) 1999-2003 Ng Pheng Siong. All rights reserved.""" + +import sys +from M2Crypto import m2, RSA +_RSA = RSA +del RSA + + +class RSA(_RSA.RSA): + pass + + +class RSA_pub(_RSA.RSA_pub): + pass + + +def new_pub_key((e, n)): + """ + Factory function that instantiates an RSA_pub object from a (e, n) tuple. + + 'e' is the RSA public exponent; it is a string in OpenSSL's binary format, + i.e., a number of bytes in big-endian. + + 'n' is the RSA composite of primes; it is a string in OpenSSL's binary format, + i.e., a number of bytes in big-endian. + """ + import warnings + warnings.warn('Deprecated. No maintainer for PGP. If you use this, please inform M2Crypto maintainer.', DeprecationWarning) + + rsa = m2.rsa_new() + m2.rsa_set_e_bin(rsa, e) + m2.rsa_set_n_bin(rsa, n) + return RSA_pub(rsa, 1) + diff --git a/M2Crypto/PGP/__init__.py b/M2Crypto/PGP/__init__.py new file mode 100644 index 0000000..27fd669 --- /dev/null +++ b/M2Crypto/PGP/__init__.py @@ -0,0 +1,14 @@ +"""M2Crypto PGP2. + +Copyright (c) 1999-2003 Ng Pheng Siong. All rights reserved.""" + +from constants import * + +from packet import public_key_packet, trust_packet, userid_packet,\ + comment_packet, signature_packet, private_key_packet, cke_packet,\ + pke_packet, literal_packet, packet_stream + +from PublicKey import * +from PublicKeyRing import * + + diff --git a/M2Crypto/PGP/constants.py b/M2Crypto/PGP/constants.py new file mode 100644 index 0000000..9a7810f --- /dev/null +++ b/M2Crypto/PGP/constants.py @@ -0,0 +1,19 @@ +"""M2Crypto PGP2. + +Copyright (c) 1999-2003 Ng Pheng Siong. All rights reserved.""" + +CTB_TAG = 128 + +CTB_PKE = 1 +CTB_SIGNATURE = 2 +CTB_MESSAGE_DIGETS = 3 +CTB_PRIVATE_KEY = 5 +CTB_PUBLIC_KEY = 6 +CTB_COMPRESSED_DATA = 8 +CTB_CKE = 9 +CTB_LITERAL_DATA = 11 +CTB_TRUST = 12 +CTB_USERID = 13 +CTB_COMMENT = 14 + + diff --git a/M2Crypto/PGP/packet.py b/M2Crypto/PGP/packet.py new file mode 100644 index 0000000..d662c84 --- /dev/null +++ b/M2Crypto/PGP/packet.py @@ -0,0 +1,379 @@ +"""M2Crypto PGP2. + +This module implements PGP packets per RFC1991 and various source distributions. + +Each packet type is represented by a class; packet classes derive from +the abstract 'packet' class. + +The 'message digest' packet type, mentioned but not documented in RFC1991, +is not implemented. + +Copyright (c) 1999-2003 Ng Pheng Siong. All rights reserved.""" + +# XXX Work-in-progress. + +# Be liberal in what you accept. +# Be conservative in what you send. +# Be lazy in what you eval. + +import struct, time + +try: + from cStringIO import StringIO +except ImportError: + from StringIO import StringIO + +from M2Crypto import EVP, RSA +from M2Crypto.util import octx_to_num + +from constants import * + +_OK_VERSION = ('\002', '\003') +_OK_VALIDITY = ('\000',) +_OK_PKC = ('\001',) + + +class packet: + def __init__(self, ctb, body=None): + import warnings + warnings.warn('Deprecated. No maintainer for PGP. If you use this, please inform M2Crypto maintainer.', DeprecationWarning) + + self.ctb = ctb + if body is not None: + self.body = StringIO(body) + else: + self.body = None + + def validate(self): + return 1 + + def pack(self): + raise NotImplementedError, '%s.pack(): abstract method' % (self.__class__,) + + def version(self): + if hasattr(self, '_version'): + return ord(self._version) + else: + return None + + def timestamp(self): + if hasattr(self, '_timestamp'): + return struct.unpack('>L', self._timestamp)[0] + else: + return None + + def validity(self): + if hasattr(self, '_validity'): + return struct.unpack('>H', self._validity)[0] + else: + return None + + def pkc(self): + if hasattr(self, '_pkc'): + return self._pkc + else: + return None + + def _llf(self, lenf): + if lenf < 256: + return (0, chr(lenf)) + elif lenf < 65536: + return (1, struct.pack('>H', lenf)) + else: + assert lenf < 2L**32 + return (2, struct.pack('>L', lenf)) + + def _ctb(self, llf): + ctbv = _FACTORY[self.__class__] + return chr((1 << 7) | (ctbv << 2) | llf) + + +class public_key_packet(packet): + def __init__(self, ctb, body=None): + packet.__init__(self, ctb, body) + if self.body is not None: + self._version = self.body.read(1) + self._timestamp = self.body.read(4) + self._validity = self.body.read(2) + self._pkc = self.body.read(1) + + self._nlen = self.body.read(2) + nlen = (struct.unpack('>H', self._nlen)[0] + 7) / 8 + self._n = self.body.read(nlen) + + self._elen = self.body.read(2) + elen = (struct.unpack('>H', self._elen)[0] + 7) / 8 + self._e = self.body.read(elen) + + def pack(self): + if self.body is None: + self.body = StringIO() + self.body.write(self._version) + self.body.write(self._timestamp) + self.body.write(self._validity) + self.body.write(self._pkc) + self.body.write(self._nlen) + self.body.write(self._n) + self.body.write(self._elen) + self.body.write(self._e) + self.body = self.body.getvalue() + llf, lenf = self._llf(len(self.body)) + ctb = self._ctb(llf) + return '%s%s%s' % (ctb, lenf, self.body) + + def pubkey(self): + return self._pubkey.pub() + + +class trust_packet(packet): + # This implementation neither interprets nor emits trust packets. + def __init__(self, ctb, body=None): + packet.__init__(self, ctb, body) + if body is not None: + self.trust = self.body.read(1) + + +class userid_packet(packet): + def __init__(self, ctb, body=None): + packet.__init__(self, ctb, body) + if body is not None: + self._userid = body + + def pack(self): + if self.body is None: + self.body = StringIO() + self.body.write(chr(len(self._userid))) + self.body.write(self._userid) + self.body = self.body.getvalue() + return self.ctb + self.body + + def userid(self): + return self._userid + + +class comment_packet(packet): + def __init__(self, ctb, body=None): + packet.__init__(self, ctb, body) + if body is not None: + self.comment = self.body.getvalue() + + def pack(self): + if self.body is None: + self.body = StringIO() + self.body.write(chr(len(self.comment))) + self.body.write(self.comment) + self.body = self.body.getvalue() + return self.ctb + self.body + + +class signature_packet(packet): + def __init__(self, ctb, body=None): + packet.__init__(self, ctb, body) + if body is not None: + self._version = self.body.read(1) + self._len_md_stuff = self.body.read(1) + self._classification = self.body.read(1) + self._timestamp = self.body.read(4) + self._keyid = self.body.read(8) + self._pkc = self.body.read(1) + self._md_algo = self.body.read(1) + self._md_chksum = self.body.read(2) + self._sig = self.body.read() + + def pack(self): + if self.body is None: + self.body = StringIO() + self.body.write(self._version) + self.body.write(self._len_md_stuff) + self.body.write(self._classification) + self.body.write(self._timestamp) + self.body.write(self._keyid) + self.body.write(self._pkc) + self.body.write(self._md_algo) + self.body.write(self._md_chksum) + self.body.write(self._sig) + self.body = self.body.getvalue() + llf, lenf = self._llf(len(body)) + self.ctb = self.ctb | llf + return '%s%s%s' % (self.ctb, lenf, self.body) + + + def validate(self): + if self._version not in _OK_VERSION: + return None + if self._len_md_stuff != '\005': + return None + + +class private_key_packet(packet): + def __init__(self, ctb, body=None): + packet.__init__(self, ctb, body) + if body is not None: + self._version = self.body.read(1) + self._timestamp = self.body.read(4) + self._validity = self.body.read(2) + self._pkc = self.body.read(1) + + self._nlen = self.body.read(2) + nlen = (struct.unpack('>H', self._nlen)[0] + 7) / 8 + self._n = self.body.read(nlen) + + self._elen = self.body.read(2) + elen = (struct.unpack('>H', self._elen)[0] + 7) / 8 + self._e = self.body.read(elen) + + self._cipher = self.body.read(1) + if self._cipher == '\001': + self._iv = self.body.read(8) + else: + self._iv = None + + for param in ['d', 'p', 'q', 'u']: + _plen = self.body.read(2) + setattr(self, '_'+param+'len', _plen) + plen = (struct.unpack('>H', _plen)[0] + 7) / 8 + setattr(self, '_'+param, self.body.read(plen)) + + self._cksum = self.body.read(2) + + def is_encrypted(self): + return ord(self._cipher) + + +class cke_packet(packet): + def __init__(self, ctb, body=None): + packet.__init__(self, ctb, body) + if body is not None: + self._iv = self.body.read(8) + self._cksum = self.body.read(2) + self._ctxt = self.body.read() + + +class pke_packet(packet): + def __init__(self, ctb, body=None): + packet.__init__(self, ctb, body) + if body is not None: + self._version = self.body.read(1) + self._keyid = self.body.read(8) + self._pkc = ord(self.body.read(1)) + + deklen = (struct.unpack('>H', self.body.read(2))[0] + 7 ) / 8 + self._dek = octx_to_num(self.body.read(deklen)) + + +class literal_packet(packet): + def __init__(self, ctb, body=None): + packet.__init__(self, ctb, body) + if body is not None: + self.fmode = self.body.read(1) + fnlen = self.body.read(1) + self.fname = self.body.read(fnlen) + self.ftime = self.body.read(4) + #self.data = self.body.read() + + +class compressed_packet(packet): + def __init__(self, ctb, stream): + packet.__init__(self, ctb, '') + if body is not None: + self.algo = stream.read(1) + # This reads the entire stream into memory. + self.data = stream.read() + + def validate(self): + return (self.algo == '\001') + + def uncompress(self): + import zlib + decomp = zlib.decompressobj(-13) # RFC 2440, pg 61. + # This doubles the memory usage. + stream = StringIO(decomp.decompress(self.data)) + return stream + + +_FACTORY = { + 1 : pke_packet, + 2 : signature_packet, + #3 : message_digest_packet, # XXX not implemented + 5 : private_key_packet, + 6 : public_key_packet, + #8 : compressed_packet, # special case + 9 : cke_packet, + 11 : literal_packet, + 12 : trust_packet, + 13 : userid_packet, + 14 : comment_packet, + pke_packet : 1, + signature_packet : 2, + #3 : message_digest_packet, + private_key_packet : 5, + public_key_packet : 6, + #8 : compressed_packet, + cke_packet : 9, + literal_packet : 11, + trust_packet : 12, + userid_packet : 13, + comment_packet : 14 +} + + +class packet_stream: + def __init__(self, input): + self.stream = input + self.under_current = None + self._count = 0 + + def close(self): + self.stream.close() + if self.under_current is not None: + self.under_current.close() + + def read(self, keep_trying=0): + while 1: + ctb0 = self.stream.read(1) + if not ctb0: + return None + ctb = ord(ctb0) + if is_ctb(ctb): + break + elif keep_trying: + continue + else: + raise XXXError + ctbt = (ctb & 0x3c) >> 2 + + if ctbt == CTB_COMPRESSED_DATA: + self.under_current = self.stream + cp = compressed_packet(ctb0, self.stream) + self.stream = cp.uncompress() + return self.read() + + # Decode the length of following data. See RFC for details. + llf = ctb & 3 + if llf == 0: + lenf = ord(self.stream.read(1)) + elif llf == 1: + lenf = struct.unpack('>H', self.stream.read(2))[0] + elif llf == 2: + lenf = struct.unpack('>L', self.stream.read(4))[0] + else: # llf == 3 + raise XXXError, 'impossible case' + + body = self.stream.read(lenf) + if not body or (len(body) != lenf): + raise XXXError, 'corrupted packet' + + self._count = self.stream.tell() + try: + return _FACTORY[ctbt](ctb0, body) + except KeyError: + return packet(ctb0, body) + + def count(self): + return self._count + +def is_ctb(ctb): + return ctb & 0xc0 + +def make_ctb(value, llf): + return chr((1 << 7) | (value << 2) | llf) diff --git a/M2Crypto/RC4.py b/M2Crypto/RC4.py new file mode 100644 index 0000000..1b5d408 --- /dev/null +++ b/M2Crypto/RC4.py @@ -0,0 +1,31 @@ +"""M2Crypto wrapper for OpenSSL RC4 API. + +Copyright (c) 1999-2003 Ng Pheng Siong. All rights reserved.""" + +from m2 import rc4_new, rc4_free, rc4_set_key, rc4_update + +class RC4: + + """Object interface to the stream cipher RC4.""" + + rc4_free = rc4_free + + def __init__(self, key=None): + self.cipher = rc4_new() + if key: + rc4_set_key(self.cipher, key) + + def __del__(self): + if getattr(self, 'cipher', None): + self.rc4_free(self.cipher) + + def set_key(self, key): + rc4_set_key(self.cipher, key) + + def update(self, data): + return rc4_update(self.cipher, data) + + def final(self): + return '' + + diff --git a/M2Crypto/RSA.py b/M2Crypto/RSA.py new file mode 100644 index 0000000..2dff160 --- /dev/null +++ b/M2Crypto/RSA.py @@ -0,0 +1,448 @@ +"""M2Crypto wrapper for OpenSSL RSA API. + +Copyright (c) 1999-2004 Ng Pheng Siong. All rights reserved.""" + +import sys +import util, BIO, Err, m2 + +class RSAError(Exception): pass + +m2.rsa_init(RSAError) + +no_padding = m2.no_padding +pkcs1_padding = m2.pkcs1_padding +sslv23_padding = m2.sslv23_padding +pkcs1_oaep_padding = m2.pkcs1_oaep_padding + + +class RSA: + """ + RSA Key Pair. + """ + + m2_rsa_free = m2.rsa_free + + def __init__(self, rsa, _pyfree=0): + assert m2.rsa_type_check(rsa), "'rsa' type error" + self.rsa = rsa + self._pyfree = _pyfree + + def __del__(self): + if getattr(self, '_pyfree', 0): + self.m2_rsa_free(self.rsa) + + def __len__(self): + return m2.rsa_size(self.rsa) << 3 + + def __getattr__(self, name): + if name == 'e': + return m2.rsa_get_e(self.rsa) + elif name == 'n': + return m2.rsa_get_n(self.rsa) + else: + raise AttributeError + + def pub(self): + assert self.check_key(), 'key is not initialised' + return m2.rsa_get_e(self.rsa), m2.rsa_get_n(self.rsa) + + def public_encrypt(self, data, padding): + assert self.check_key(), 'key is not initialised' + return m2.rsa_public_encrypt(self.rsa, data, padding) + + def public_decrypt(self, data, padding): + assert self.check_key(), 'key is not initialised' + return m2.rsa_public_decrypt(self.rsa, data, padding) + + def private_encrypt(self, data, padding): + assert self.check_key(), 'key is not initialised' + return m2.rsa_private_encrypt(self.rsa, data, padding) + + def private_decrypt(self, data, padding): + assert self.check_key(), 'key is not initialised' + return m2.rsa_private_decrypt(self.rsa, data, padding) + + def save_key_bio(self, bio, cipher='aes_128_cbc', callback=util.passphrase_callback): + """ + Save the key pair to an M2Crypto.BIO.BIO object in PEM format. + + @type bio: M2Crypto.BIO.BIO + @param bio: M2Crypto.BIO.BIO object to save key to. + + @type cipher: string + @param cipher: Symmetric cipher to protect the key. The default + cipher is 'aes_128_cbc'. If cipher is None, then the key is saved + in the clear. + + @type callback: Python callable + @param callback: A Python callable object that is invoked + to acquire a passphrase with which to protect the key. + The default is util.passphrase_callback. + """ + if cipher is None: + return m2.rsa_write_key_no_cipher(self.rsa, bio._ptr(), callback) + else: + ciph = getattr(m2, cipher, None) + if ciph is None: + raise RSAError, 'not such cipher %s' % cipher + else: + ciph = ciph() + return m2.rsa_write_key(self.rsa, bio._ptr(), ciph, callback) + + def save_key(self, file, cipher='aes_128_cbc', callback=util.passphrase_callback): + """ + Save the key pair to a file in PEM format. + + @type file: string + @param file: Name of file to save key to. + + @type cipher: string + @param cipher: Symmetric cipher to protect the key. The default + cipher is 'aes_128_cbc'. If cipher is None, then the key is saved + in the clear. + + @type callback: Python callable + @param callback: A Python callable object that is invoked + to acquire a passphrase with which to protect the key. + The default is util.passphrase_callback. + """ + bio = BIO.openfile(file, 'wb') + return self.save_key_bio(bio, cipher, callback) + + save_pem = save_key + + def as_pem(self, cipher='aes_128_cbc', callback=util.passphrase_callback): + """ + Returns the key(pair) as a string in PEM format. + """ + bio = BIO.MemoryBuffer() + self.save_key_bio(bio, cipher, callback) + return bio.read() + + def save_key_der_bio(self, bio): + """ + Save the key pair to an M2Crypto.BIO.BIO object in DER format. + + @type bio: M2Crypto.BIO.BIO + @param bio: M2Crypto.BIO.BIO object to save key to. + """ + return m2.rsa_write_key_der(self.rsa, bio._ptr()) + + def save_key_der(self, file): + """ + Save the key pair to a file in DER format. + + @type file: str + @param file: Filename to save key to + """ + bio = BIO.openfile(file, 'wb') + return self.save_key_der_bio(bio) + + def save_pub_key_bio(self, bio): + """ + Save the public key to an M2Crypto.BIO.BIO object in PEM format. + + @type bio: M2Crypto.BIO.BIO + @param bio: M2Crypto.BIO.BIO object to save key to. + """ + return m2.rsa_write_pub_key(self.rsa, bio._ptr()) + + def save_pub_key(self, file): + """ + Save the public key to a file in PEM format. + + @type file: string + @param file: Name of file to save key to. + """ + bio = BIO.openfile(file, 'wb') + return m2.rsa_write_pub_key(self.rsa, bio._ptr()) + + def check_key(self): + return m2.rsa_check_key(self.rsa) + + def sign_rsassa_pss(self, digest, algo='sha1', salt_length=20): + """ + Signs a digest with the private key using RSASSA-PSS + + @requires: OpenSSL 0.9.7h or later. + + @type digest: str + @param digest: A digest created by using the digest method + + @type salt_length: int + @param salt_length: The length of the salt to use + + @type algo: str + @param algo: The hash algorithm to use + + @return: a string which is the signature + """ + hash = getattr(m2, algo, None) + if hash is None: + raise ValueError('not such hash algorithm %s' % hash_algo) + + signature = m2.rsa_padding_add_pkcs1_pss(self.rsa, digest, hash(), salt_length) + + return self.private_encrypt(signature, m2.no_padding) + + def verify_rsassa_pss(self, data, signature, algo='sha1', salt_length=20): + """ + Verifies the signature RSASSA-PSS + + @requires: OpenSSL 0.9.7h or later. + + @type data: str + @param data: Data that has been signed + + @type signature: str + @param signature: The signature signed with RSASSA-PSS + + @type salt_length: int + @param salt_length: The length of the salt that was used + + @type algo: str + @param algo: The hash algorithm to use + + @return: 1 or 0, depending on whether the signature was + verified or not. + """ + hash = getattr(m2, algo, None) + if hash is None: + raise ValueError('not such hash algorithm %s' % hash_algo) + + plain_signature = self.public_decrypt(signature, m2.no_padding) + + return m2.rsa_verify_pkcs1_pss(self.rsa, data, plain_signature, hash(), salt_length) + + def sign(self, digest, algo='sha1'): + """ + Signs a digest with the private key + + @type digest: str + @param digest: A digest created by using the digest method + + @type algo: str + @param algo: The method that created the digest. + Legal values are 'sha1','sha224', 'sha256', 'ripemd160', + and 'md5'. + + @return: a string which is the signature + """ + digest_type = getattr(m2, 'NID_' + algo, None) + if digest_type is None: + raise ValueError, ('unknown algorithm', algo) + + return m2.rsa_sign(self.rsa, digest, digest_type) + + def verify(self, data, signature, algo='sha1'): + """ + Verifies the signature with the public key + + @type data: str + @param data: Data that has been signed + + @type signature: str + @param signature: The signature signed with the private key + + @type algo: str + @param algo: The method use to create digest from the data + before it was signed. Legal values are 'sha1','sha224', + 'sha256', 'ripemd160', and 'md5'. + + @return: True or False, depending on whether the signature was + verified. + """ + digest_type = getattr(m2, 'NID_' + algo, None) + if digest_type is None: + raise ValueError, ('unknown algorithm', algo) + + return m2.rsa_verify(self.rsa, data, signature, digest_type) + + +class RSA_pub(RSA): + + """ + Object interface to an RSA public key. + """ + + def __setattr__(self, name, value): + if name in ['e', 'n']: + raise RSAError, \ + 'use factory function new_pub_key() to set (e, n)' + else: + self.__dict__[name] = value + + def private_encrypt(self, *argv): + raise RSAError, 'RSA_pub object has no private key' + + def private_decrypt(self, *argv): + raise RSAError, 'RSA_pub object has no private key' + + def save_key(self, file, *args, **kw): + """ + Save public key to file. + """ + return self.save_pub_key(file) + + def save_key_bio(self, bio, *args, **kw): + """ + Save public key to BIO. + """ + return self.save_pub_key_bio(bio) + + #save_key_der + + #save_key_der_bio + + def check_key(self): + return m2.rsa_check_pub_key(self.rsa) + + +def rsa_error(): + raise RSAError, m2.err_reason_error_string(m2.err_get_error()) + + +def keygen_callback(p, n, out=sys.stdout): + """ + Default callback for gen_key(). + """ + ch = ['.','+','*','\n'] + out.write(ch[p]) + out.flush() + + +def gen_key(bits, e, callback=keygen_callback): + """ + Generate an RSA key pair. + + @type bits: int + @param bits: Key length, in bits. + + @type e: int + @param e: The RSA public exponent. + + @type callback: Python callable + @param callback: A Python callable object that is invoked + during key generation; its usual purpose is to provide visual + feedback. The default callback is keygen_callback. + + @rtype: M2Crypto.RSA.RSA + @return: M2Crypto.RSA.RSA object. + """ + return RSA(m2.rsa_generate_key(bits, e, callback), 1) + + +def load_key(file, callback=util.passphrase_callback): + """ + Load an RSA key pair from file. + + @type file: string + @param file: Name of file containing RSA public key in PEM format. + + @type callback: Python callable + @param callback: A Python callable object that is invoked + to acquire a passphrase with which to unlock the key. + The default is util.passphrase_callback. + + @rtype: M2Crypto.RSA.RSA + @return: M2Crypto.RSA.RSA object. + """ + bio = BIO.openfile(file) + return load_key_bio(bio, callback) + + +def load_key_bio(bio, callback=util.passphrase_callback): + """ + Load an RSA key pair from an M2Crypto.BIO.BIO object. + + @type bio: M2Crypto.BIO.BIO + @param bio: M2Crypto.BIO.BIO object containing RSA key pair in PEM + format. + + @type callback: Python callable + @param callback: A Python callable object that is invoked + to acquire a passphrase with which to unlock the key. + The default is util.passphrase_callback. + + @rtype: M2Crypto.RSA.RSA + @return: M2Crypto.RSA.RSA object. + """ + rsa = m2.rsa_read_key(bio._ptr(), callback) + if rsa is None: + rsa_error() + return RSA(rsa, 1) + + +def load_key_string(string, callback=util.passphrase_callback): + """ + Load an RSA key pair from a string. + + @type string: string + @param string: String containing RSA key pair in PEM format. + + @type callback: Python callable + @param callback: A Python callable object that is invoked + to acquire a passphrase with which to unlock the key. + The default is util.passphrase_callback. + + @rtype: M2Crypto.RSA.RSA + @return: M2Crypto.RSA.RSA object. + """ + bio = BIO.MemoryBuffer(string) + return load_key_bio(bio, callback) + + +def load_pub_key(file): + """ + Load an RSA public key from file. + + @type file: string + @param file: Name of file containing RSA public key in PEM format. + + @rtype: M2Crypto.RSA.RSA_pub + @return: M2Crypto.RSA.RSA_pub object. + """ + bio = BIO.openfile(file) + return load_pub_key_bio(bio) + + +def load_pub_key_bio(bio): + """ + Load an RSA public key from an M2Crypto.BIO.BIO object. + + @type bio: M2Crypto.BIO.BIO + @param bio: M2Crypto.BIO.BIO object containing RSA public key in PEM + format. + + @rtype: M2Crypto.RSA.RSA_pub + @return: M2Crypto.RSA.RSA_pub object. + """ + rsa = m2.rsa_read_pub_key(bio._ptr()) + if rsa is None: + rsa_error() + return RSA_pub(rsa, 1) + + +def new_pub_key((e, n)): + """ + Instantiate an RSA_pub object from an (e, n) tuple. + + @type e: string + @param e: The RSA public exponent; it is a string in OpenSSL's MPINT + format - 4-byte big-endian bit-count followed by the appropriate + number of bits. + + @type n: string + @param n: The RSA composite of primes; it is a string in OpenSSL's MPINT + format - 4-byte big-endian bit-count followed by the appropriate + number of bits. + + @rtype: M2Crypto.RSA.RSA_pub + @return: M2Crypto.RSA.RSA_pub object. + """ + rsa = m2.rsa_new() + m2.rsa_set_e(rsa, e) + m2.rsa_set_n(rsa, n) + return RSA_pub(rsa, 1) + + diff --git a/M2Crypto/Rand.py b/M2Crypto/Rand.py new file mode 100644 index 0000000..1e6a918 --- /dev/null +++ b/M2Crypto/Rand.py @@ -0,0 +1,17 @@ +"""M2Crypto wrapper for OpenSSL PRNG. Requires OpenSSL 0.9.5 and above. + +Copyright (c) 1999-2003 Ng Pheng Siong. All rights reserved.""" + +__all__ = ['rand_seed', 'rand_add', 'load_file', 'save_file', 'rand_bytes', + 'rand_pseudo_bytes'] + +import m2 + +rand_seed = m2.rand_seed +rand_add = m2.rand_add +load_file = m2.rand_load_file +save_file = m2.rand_save_file +rand_bytes = m2.rand_bytes +rand_pseudo_bytes = m2.rand_pseudo_bytes + + diff --git a/M2Crypto/SMIME.py b/M2Crypto/SMIME.py new file mode 100644 index 0000000..556cd8f --- /dev/null +++ b/M2Crypto/SMIME.py @@ -0,0 +1,245 @@ +"""M2Crypto wrapper for OpenSSL S/MIME API. + +Copyright (c) 1999-2003 Ng Pheng Siong. All rights reserved.""" + +import BIO, EVP, X509, Err, util +import m2 + +PKCS7_TEXT = m2.PKCS7_TEXT +PKCS7_NOCERTS = m2.PKCS7_NOCERTS +PKCS7_NOSIGS = m2.PKCS7_NOSIGS +PKCS7_NOCHAIN = m2.PKCS7_NOCHAIN +PKCS7_NOINTERN = m2.PKCS7_NOINTERN +PKCS7_NOVERIFY = m2.PKCS7_NOVERIFY +PKCS7_DETACHED = m2.PKCS7_DETACHED +PKCS7_BINARY = m2.PKCS7_BINARY +PKCS7_NOATTR = m2.PKCS7_NOATTR + +PKCS7_SIGNED = m2.PKCS7_SIGNED +PKCS7_ENVELOPED = m2.PKCS7_ENVELOPED +PKCS7_SIGNED_ENVELOPED = m2.PKCS7_SIGNED_ENVELOPED # Deprecated +PKCS7_DATA = m2.PKCS7_DATA + +class PKCS7_Error(Exception): pass + +m2.pkcs7_init(PKCS7_Error) + +class PKCS7: + + m2_pkcs7_free = m2.pkcs7_free + + def __init__(self, pkcs7=None, _pyfree=0): + if pkcs7 is not None: + self.pkcs7 = pkcs7 + self._pyfree = _pyfree + else: + self.pkcs7 = m2.pkcs7_new() + self._pyfree = 1 + + def __del__(self): + if getattr(self, '_pyfree', 0): + self.m2_pkcs7_free(self.pkcs7) + + def _ptr(self): + return self.pkcs7 + + def type(self, text_name=0): + if text_name: + return m2.pkcs7_type_sn(self.pkcs7) + else: + return m2.pkcs7_type_nid(self.pkcs7) + + def write(self, bio): + return m2.pkcs7_write_bio(self.pkcs7, bio._ptr()) + + def write_der(self, bio): + return m2.pkcs7_write_bio_der(self.pkcs7, bio._ptr()) + + def get0_signers(self, certs, flags = 0): + return X509.X509_Stack(m2.pkcs7_get0_signers(self.pkcs7, + certs.stack, flags), 1) + + +def load_pkcs7(p7file): + bio = m2.bio_new_file(p7file, 'r') + if bio is None: + raise BIO.BIOError(Err.get_error()) + + try: + p7_ptr = m2.pkcs7_read_bio(bio) + finally: + m2.bio_free(bio) + + if p7_ptr is None: + raise PKCS7_Error(Err.get_error()) + return PKCS7(p7_ptr, 1) + + +def load_pkcs7_bio(p7_bio): + p7_ptr = m2.pkcs7_read_bio(p7_bio._ptr()) + if p7_ptr is None: + raise PKCS7_Error(Err.get_error()) + return PKCS7(p7_ptr, 1) + + +def smime_load_pkcs7(p7file): + bio = m2.bio_new_file(p7file, 'r') + if bio is None: + raise BIO.BIOError(Err.get_error()) + + try: + p7_ptr, bio_ptr = m2.smime_read_pkcs7(bio) + finally: + m2.bio_free(bio) + + if p7_ptr is None: + raise SMIME_Error(Err.get_error()) + if bio_ptr is None: + return PKCS7(p7_ptr, 1), None + else: + return PKCS7(p7_ptr, 1), BIO.BIO(bio_ptr, 1) + + +def smime_load_pkcs7_bio(p7_bio): + p7_ptr, bio_ptr = m2.smime_read_pkcs7(p7_bio._ptr()) + if p7_ptr is None: + raise SMIME_Error(Err.get_error()) + if bio_ptr is None: + return PKCS7(p7_ptr, 1), None + else: + return PKCS7(p7_ptr, 1), BIO.BIO(bio_ptr, 1) + + +class Cipher: + + """ + Object interface to EVP_CIPHER without all the frills of M2Crypto.EVP.Cipher. + """ + + def __init__(self, algo): + cipher = getattr(m2, algo, None) + if cipher is None: + raise ValueError, ('unknown cipher', algo) + self.cipher = cipher() + + def _ptr(self): + return self.cipher + + +class SMIME_Error(Exception): pass + +m2.smime_init(SMIME_Error) + +class SMIME: + def load_key(self, keyfile, certfile=None, callback=util.passphrase_callback): + if certfile is None: + certfile = keyfile + self.pkey = EVP.load_key(keyfile, callback) + self.x509 = X509.load_cert(certfile) + + def load_key_bio(self, keybio, certbio=None, callback=util.passphrase_callback): + if certbio is None: + certbio = keybio + self.pkey = EVP.load_key_bio(keybio, callback) + self.x509 = X509.load_cert_bio(certbio) + + def set_x509_stack(self, stack): + assert isinstance(stack, X509.X509_Stack) + self.x509_stack = stack + + def set_x509_store(self, store): + assert isinstance(store, X509.X509_Store) + self.x509_store = store + + def set_cipher(self, cipher): + assert isinstance(cipher, Cipher) + self.cipher = cipher + + def unset_key(self): + del self.pkey + del self.x509 + + def unset_x509_stack(self): + del self.x509_stack + + def unset_x509_store(self): + del self.x509_store + + def unset_cipher(self): + del self.cipher + + def encrypt(self, data_bio, flags=0): + if not hasattr(self, 'cipher'): + raise SMIME_Error, 'no cipher: use set_cipher()' + if not hasattr(self, 'x509_stack'): + raise SMIME_Error, 'no recipient certs: use set_x509_stack()' + pkcs7 = m2.pkcs7_encrypt(self.x509_stack._ptr(), data_bio._ptr(), self.cipher._ptr(), flags) + if pkcs7 is None: + raise SMIME_Error(Err.get_error()) + return PKCS7(pkcs7, 1) + + def decrypt(self, pkcs7, flags=0): + if not hasattr(self, 'pkey'): + raise SMIME_Error, 'no private key: use load_key()' + if not hasattr(self, 'x509'): + raise SMIME_Error, 'no certificate: load_key() used incorrectly?' + blob = m2.pkcs7_decrypt(pkcs7._ptr(), self.pkey._ptr(), self.x509._ptr(), flags) + if blob is None: + raise SMIME_Error(Err.get_error()) + return blob + + def sign(self, data_bio, flags=0): + if not hasattr(self, 'pkey'): + raise SMIME_Error, 'no private key: use load_key()' + if hasattr(self, 'x509_stack'): + pkcs7 = m2.pkcs7_sign1(self.x509._ptr(), self.pkey._ptr(), + self.x509_stack._ptr(), data_bio._ptr(), flags) + if pkcs7 is None: + raise SMIME_Error(Err.get_error()) + return PKCS7(pkcs7, 1) + else: + pkcs7 = m2.pkcs7_sign0(self.x509._ptr(), self.pkey._ptr(), + data_bio._ptr(), flags) + if pkcs7 is None: + raise SMIME_Error(Err.get_error()) + return PKCS7(pkcs7, 1) + + def verify(self, pkcs7, data_bio=None, flags=0): + if not hasattr(self, 'x509_stack'): + raise SMIME_Error, 'no signer certs: use set_x509_stack()' + if not hasattr(self, 'x509_store'): + raise SMIME_Error, 'no x509 cert store: use set_x509_store()' + assert isinstance(pkcs7, PKCS7), 'pkcs7 not an instance of PKCS7' + p7 = pkcs7._ptr() + if data_bio is None: + blob = m2.pkcs7_verify0(p7, self.x509_stack._ptr(), self.x509_store._ptr(), flags) + else: + blob = m2.pkcs7_verify1(p7, self.x509_stack._ptr(), self.x509_store._ptr(), data_bio._ptr(), flags) + if blob is None: + raise SMIME_Error(Err.get_error()) + return blob + + def write(self, out_bio, pkcs7, data_bio=None, flags=0): + assert isinstance(pkcs7, PKCS7) + if data_bio is None: + return m2.smime_write_pkcs7(out_bio._ptr(), pkcs7._ptr(), flags) + else: + return m2.smime_write_pkcs7_multi(out_bio._ptr(), pkcs7._ptr(), data_bio._ptr(), flags) + + +def text_crlf(text): + bio_in = BIO.MemoryBuffer(text) + bio_out = BIO.MemoryBuffer() + if m2.smime_crlf_copy(bio_in._ptr(), bio_out._ptr()): + return bio_out.read() + else: + raise SMIME_Error(Err.get_error()) + + +def text_crlf_bio(bio_in): + bio_out = BIO.MemoryBuffer() + if m2.smime_crlf_copy(bio_in._ptr(), bio_out._ptr()): + return bio_out + else: + raise SMIME_Error(Err.get_error()) + diff --git a/M2Crypto/SSL/Checker.py b/M2Crypto/SSL/Checker.py new file mode 100644 index 0000000..5218663 --- /dev/null +++ b/M2Crypto/SSL/Checker.py @@ -0,0 +1,224 @@ +""" +SSL peer certificate checking routines + +Copyright (c) 2004-2007 Open Source Applications Foundation. +All rights reserved. + +Copyright 2008 Heikki Toivonen. All rights reserved. +""" + +__all__ = ['SSLVerificationError', 'NoCertificate', 'WrongCertificate', + 'WrongHost', 'Checker'] + +from M2Crypto import util, EVP, m2 +import re + +class SSLVerificationError(Exception): + pass + +class NoCertificate(SSLVerificationError): + pass + +class WrongCertificate(SSLVerificationError): + pass + +class WrongHost(SSLVerificationError): + def __init__(self, expectedHost, actualHost, fieldName='commonName'): + """ + This exception will be raised if the certificate returned by the + peer was issued for a different host than we tried to connect to. + This could be due to a server misconfiguration or an active attack. + + @param expectedHost: The name of the host we expected to find in the + certificate. + @param actualHost: The name of the host we actually found in the + certificate. + @param fieldName: The field name where we noticed the error. This + should be either 'commonName' or 'subjectAltName'. + """ + if fieldName not in ('commonName', 'subjectAltName'): + raise ValueError('Unknown fieldName, should be either commonName or subjectAltName') + + SSLVerificationError.__init__(self) + self.expectedHost = expectedHost + self.actualHost = actualHost + self.fieldName = fieldName + + def __str__(self): + s = 'Peer certificate %s does not match host, expected %s, got %s' \ + % (self.fieldName, self.expectedHost, self.actualHost) + if isinstance(s, unicode): + s = s.encode('utf8') + return s + + +class Checker: + + numericIpMatch = re.compile('^[0-9]+(\.[0-9]+)*$') + + def __init__(self, host=None, peerCertHash=None, peerCertDigest='sha1'): + self.host = host + self.fingerprint = peerCertHash + self.digest = peerCertDigest + + def __call__(self, peerCert, host=None): + if peerCert is None: + raise NoCertificate('peer did not return certificate') + + if host is not None: + self.host = host + + if self.fingerprint: + if self.digest not in ('sha1', 'md5'): + raise ValueError('unsupported digest "%s"' %(self.digest)) + + if (self.digest == 'sha1' and len(self.fingerprint) != 40) or \ + (self.digest == 'md5' and len(self.fingerprint) != 32): + raise WrongCertificate('peer certificate fingerprint length does not match') + + der = peerCert.as_der() + md = EVP.MessageDigest(self.digest) + md.update(der) + digest = md.final() + if util.octx_to_num(digest) != int(self.fingerprint, 16): + raise WrongCertificate('peer certificate fingerprint does not match') + + if self.host: + hostValidationPassed = False + self.useSubjectAltNameOnly = False + + # subjectAltName=DNS:somehost[, ...]* + try: + subjectAltName = peerCert.get_ext('subjectAltName').get_value() + if self._splitSubjectAltName(self.host, subjectAltName): + hostValidationPassed = True + elif self.useSubjectAltNameOnly: + raise WrongHost(expectedHost=self.host, + actualHost=subjectAltName, + fieldName='subjectAltName') + except LookupError: + pass + + # commonName=somehost[, ...]* + if not hostValidationPassed: + hasCommonName = False + commonNames = '' + for entry in peerCert.get_subject().get_entries_by_nid(m2.NID_commonName): + hasCommonName = True + commonName = entry.get_data().as_text() + if not commonNames: + commonNames = commonName + else: + commonNames += ',' + commonName + if self._match(self.host, commonName): + hostValidationPassed = True + break + + if not hasCommonName: + raise WrongCertificate('no commonName in peer certificate') + + if not hostValidationPassed: + raise WrongHost(expectedHost=self.host, + actualHost=commonNames, + fieldName='commonName') + + return True + + def _splitSubjectAltName(self, host, subjectAltName): + """ + >>> check = Checker() + >>> check._splitSubjectAltName(host='my.example.com', subjectAltName='DNS:my.example.com') + True + >>> check._splitSubjectAltName(host='my.example.com', subjectAltName='DNS:*.example.com') + True + >>> check._splitSubjectAltName(host='my.example.com', subjectAltName='DNS:m*.example.com') + True + >>> check._splitSubjectAltName(host='my.example.com', subjectAltName='DNS:m*ample.com') + False + >>> check.useSubjectAltNameOnly + True + >>> check._splitSubjectAltName(host='my.example.com', subjectAltName='DNS:m*ample.com, othername:<unsupported>') + False + >>> check._splitSubjectAltName(host='my.example.com', subjectAltName='DNS:m*ample.com, DNS:my.example.org') + False + >>> check._splitSubjectAltName(host='my.example.com', subjectAltName='DNS:m*ample.com, DNS:my.example.com') + True + >>> check._splitSubjectAltName(host='my.example.com', subjectAltName='DNS:my.example.com, DNS:my.example.org') + True + >>> check.useSubjectAltNameOnly + True + >>> check._splitSubjectAltName(host='my.example.com', subjectAltName='') + False + >>> check._splitSubjectAltName(host='my.example.com', subjectAltName='othername:<unsupported>') + False + >>> check.useSubjectAltNameOnly + False + """ + self.useSubjectAltNameOnly = False + for certHost in subjectAltName.split(','): + certHost = certHost.lower().strip() + if certHost[:4] == 'dns:': + self.useSubjectAltNameOnly = True + if self._match(host, certHost[4:]): + return True + return False + + + def _match(self, host, certHost): + """ + >>> check = Checker() + >>> check._match(host='my.example.com', certHost='my.example.com') + True + >>> check._match(host='my.example.com', certHost='*.example.com') + True + >>> check._match(host='my.example.com', certHost='m*.example.com') + True + >>> check._match(host='my.example.com', certHost='m*.EXAMPLE.com') + True + >>> check._match(host='my.example.com', certHost='m*ample.com') + False + >>> check._match(host='my.example.com', certHost='*.*.com') + False + >>> check._match(host='1.2.3.4', certHost='1.2.3.4') + True + >>> check._match(host='1.2.3.4', certHost='*.2.3.4') + False + >>> check._match(host='1234', certHost='1234') + True + """ + # XXX See RFC 2818 and 3280 for matching rules, this is may not + # XXX yet be complete. + + host = host.lower() + certHost = certHost.lower() + + if host == certHost: + return True + + if certHost.count('*') > 1: + # Not sure about this, but being conservative + return False + + if self.numericIpMatch.match(host) or \ + self.numericIpMatch.match(certHost.replace('*', '')): + # Not sure if * allowed in numeric IP, but think not. + return False + + if certHost.find('\\') > -1: + # Not sure about this, maybe some encoding might have these. + # But being conservative for now, because regex below relies + # on this. + return False + + # Massage certHost so that it can be used in regex + certHost = certHost.replace('.', '\.') + certHost = certHost.replace('*', '[^\.]*') + if re.compile('^%s$' %(certHost)).match(host): + return True + + return False + + +if __name__ == '__main__': + import doctest + doctest.testmod() diff --git a/M2Crypto/SSL/Cipher.py b/M2Crypto/SSL/Cipher.py new file mode 100644 index 0000000..2d2c088 --- /dev/null +++ b/M2Crypto/SSL/Cipher.py @@ -0,0 +1,44 @@ +"""SSL Ciphers + +Copyright (c) 1999-2003 Ng Pheng Siong. All rights reserved.""" + +__all__ = ['Cipher', 'Cipher_Stack'] + +from M2Crypto import m2 + +class Cipher: + def __init__(self, cipher): + self.cipher=cipher + + def __len__(self): + return m2.ssl_cipher_get_bits(self.cipher) + + def __repr__(self): + return "%s-%s" % (self.name(), len(self)) + + def __str__(self): + return "%s-%s" % (self.name(), len(self)) + + def version(self): + return m2.ssl_cipher_get_version(self.cipher) + + def name(self): + return m2.ssl_cipher_get_name(self.cipher) + + +class Cipher_Stack: + def __init__(self, stack): + self.stack=stack + + def __len__(self): + return m2.sk_ssl_cipher_num(self.stack) + + def __getitem__(self, idx): + if not 0 <= idx < m2.sk_ssl_cipher_num(self.stack): + raise IndexError('index out of range') + v=m2.sk_ssl_cipher_value(self.stack, idx) + return Cipher(v) + + def __iter__(self): + for i in xrange(m2.sk_ssl_cipher_num(self.stack)): + yield self[i] diff --git a/M2Crypto/SSL/Connection.py b/M2Crypto/SSL/Connection.py new file mode 100644 index 0000000..ef6a2c8 --- /dev/null +++ b/M2Crypto/SSL/Connection.py @@ -0,0 +1,361 @@ +"""SSL Connection aka socket + +Copyright (c) 1999-2004 Ng Pheng Siong. All rights reserved. + +Portions created by Open Source Applications Foundation (OSAF) are +Copyright (C) 2004-2007 OSAF. All Rights Reserved. + +Copyright 2008 Heikki Toivonen. All rights reserved. +""" + +__all__ = ['Connection', + 'timeout', # XXX Not really, but for documentation purposes + ] + +# Python +import socket + +# M2Crypto +from Cipher import Cipher, Cipher_Stack +from Session import Session +from M2Crypto import BIO, X509, m2 +import timeout +import Checker + +#SSLError = getattr(__import__('M2Crypto.SSL', globals(), locals(), 'SSLError'), 'SSLError') +from M2Crypto.SSL import SSLError + +def _serverPostConnectionCheck(*args, **kw): + return 1 + +class Connection: + + """An SSL connection.""" + + clientPostConnectionCheck = Checker.Checker() + serverPostConnectionCheck = _serverPostConnectionCheck + + m2_bio_free = m2.bio_free + m2_ssl_free = m2.ssl_free + + def __init__(self, ctx, sock=None): + self.ctx = ctx + self.ssl = m2.ssl_new(self.ctx.ctx) + if sock is not None: + self.socket = sock + else: + self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + self._fileno = self.socket.fileno() + + self.blocking = self.socket.gettimeout() + + self.ssl_close_flag = m2.bio_noclose + + + def __del__(self): + if getattr(self, 'sslbio', None): + self.m2_bio_free(self.sslbio) + if getattr(self, 'sockbio', None): + self.m2_bio_free(self.sockbio) + if self.ssl_close_flag == m2.bio_noclose and getattr(self, 'ssl', None): + self.m2_ssl_free(self.ssl) + self.socket.close() + + def close(self): + m2.ssl_shutdown(self.ssl) + + def clear(self): + """ + If there were errors in this connection, call clear() rather + than close() to end it, so that bad sessions will be cleared + from cache. + """ + return m2.ssl_clear(self.ssl) + + def set_shutdown(self, mode): + m2.ssl_set_shutdown1(self.ssl, mode) + + def get_shutdown(self): + return m2.ssl_get_shutdown(self.ssl) + + def bind(self, addr): + self.socket.bind(addr) + + def listen(self, qlen=5): + self.socket.listen(qlen) + + def ssl_get_error(self, ret): + return m2.ssl_get_error(self.ssl, ret) + + def set_bio(self, readbio, writebio): + """ + Explicitly set read and write bios + """ + m2.ssl_set_bio(self.ssl, readbio._ptr(), writebio._ptr()) + + def set_client_CA_list_from_file(self, cafile): + """ + Set the acceptable client CA list. If the client + returns a certificate, it must have been issued by + one of the CAs listed in cafile. + + Makes sense only for servers. + + @param cafile: Filename from which to load the CA list. + """ + m2.ssl_set_client_CA_list_from_file(self.ssl, cafile) + + def set_client_CA_list_from_context(self): + """ + Set the acceptable client CA list. If the client + returns a certificate, it must have been issued by + one of the CAs listed in context. + + Makes sense only for servers. + """ + m2.ssl_set_client_CA_list_from_context(self.ssl, self.ctx.ctx) + + def setup_addr(self, addr): + self.addr = addr + + def set_ssl_close_flag(self, flag): + """ + By default, SSL struct will be freed in __del__. Call with + m2.bio_close to override this default. + """ + if flag not in (m2.bio_close, m2.bio_noclose): + raise ValueError("flag must be m2.bio_close or m2.bio_noclose") + self.ssl_close_flag = flag + + def setup_ssl(self): + # Make a BIO_s_socket. + self.sockbio = m2.bio_new_socket(self.socket.fileno(), 0) + # Link SSL struct with the BIO_socket. + m2.ssl_set_bio(self.ssl, self.sockbio, self.sockbio) + # Make a BIO_f_ssl. + self.sslbio = m2.bio_new(m2.bio_f_ssl()) + # Link BIO_f_ssl with the SSL struct. + m2.bio_set_ssl(self.sslbio, self.ssl, m2.bio_noclose) + + def _setup_ssl(self, addr): + """Deprecated""" + self.setup_addr(addr) + self.setup_ssl() + + def set_accept_state(self): + m2.ssl_set_accept_state(self.ssl) + + def accept_ssl(self): + return m2.ssl_accept(self.ssl) + + def accept(self): + """Accept an SSL connection. The return value is a pair (ssl, addr) where + ssl is a new SSL connection object and addr is the address bound to + the other end of the SSL connection.""" + sock, addr = self.socket.accept() + ssl = Connection(self.ctx, sock) + ssl.addr = addr + ssl.setup_ssl() + ssl.set_accept_state() + ssl.accept_ssl() + check = getattr(self, 'postConnectionCheck', self.serverPostConnectionCheck) + if check is not None: + if not check(ssl.get_peer_cert(), ssl.addr[0]): + raise Checker.SSLVerificationError, 'post connection check failed' + return ssl, addr + + def set_connect_state(self): + m2.ssl_set_connect_state(self.ssl) + + def connect_ssl(self): + return m2.ssl_connect(self.ssl) + + def connect(self, addr): + self.socket.connect(addr) + self.addr = addr + self.setup_ssl() + self.set_connect_state() + ret = self.connect_ssl() + check = getattr(self, 'postConnectionCheck', self.clientPostConnectionCheck) + if check is not None: + if not check(self.get_peer_cert(), self.addr[0]): + raise Checker.SSLVerificationError, 'post connection check failed' + return ret + + def shutdown(self, how): + m2.ssl_set_shutdown(self.ssl, how) + + def renegotiate(self): + """Renegotiate this connection's SSL parameters.""" + return m2.ssl_renegotiate(self.ssl) + + def pending(self): + """Return the numbers of octets that can be read from the + connection.""" + return m2.ssl_pending(self.ssl) + + def _write_bio(self, data): + return m2.ssl_write(self.ssl, data) + + def _write_nbio(self, data): + return m2.ssl_write_nbio(self.ssl, data) + + def _read_bio(self, size=1024): + if size <= 0: + raise ValueError, 'size <= 0' + return m2.ssl_read(self.ssl, size) + + def _read_nbio(self, size=1024): + if size <= 0: + raise ValueError, 'size <= 0' + return m2.ssl_read_nbio(self.ssl, size) + + def write(self, data): + if self.blocking: + return self._write_bio(data) + return self._write_nbio(data) + sendall = send = write + + def read(self, size=1024): + if self.blocking: + return self._read_bio(size) + return self._read_nbio(size) + recv = read + + def setblocking(self, mode): + """Set this connection's underlying socket to _mode_.""" + self.socket.setblocking(mode) + self.blocking = mode + + def fileno(self): + return self.socket.fileno() + + def getsockopt(self, *args): + return apply(self.socket.getsockopt, args) + + def setsockopt(self, *args): + return apply(self.socket.setsockopt, args) + + def get_context(self): + """Return the SSL.Context object associated with this + connection.""" + return m2.ssl_get_ssl_ctx(self.ssl) + + def get_state(self): + """Return the SSL state of this connection.""" + return m2.ssl_get_state(self.ssl) + + def verify_ok(self): + return (m2.ssl_get_verify_result(self.ssl) == m2.X509_V_OK) + + def get_verify_mode(self): + """Return the peer certificate verification mode.""" + return m2.ssl_get_verify_mode(self.ssl) + + def get_verify_depth(self): + """Return the peer certificate verification depth.""" + return m2.ssl_get_verify_depth(self.ssl) + + def get_verify_result(self): + """Return the peer certificate verification result.""" + return m2.ssl_get_verify_result(self.ssl) + + def get_peer_cert(self): + """Return the peer certificate; if the peer did not provide + a certificate, return None.""" + c=m2.ssl_get_peer_cert(self.ssl) + if c is None: + return None + # Need to free the pointer coz OpenSSL doesn't. + return X509.X509(c, 1) + + def get_peer_cert_chain(self): + """Return the peer certificate chain; if the peer did not provide + a certificate chain, return None. + + @warning: The returned chain will be valid only for as long as the + connection object is alive. Once the connection object gets freed, + the chain will be freed as well. + """ + c=m2.ssl_get_peer_cert_chain(self.ssl) + if c is None: + return None + # No need to free the pointer coz OpenSSL does. + return X509.X509_Stack(c) + + def get_cipher(self): + """Return an M2Crypto.SSL.Cipher object for this connection; if the + connection has not been initialised with a cipher suite, return None.""" + c=m2.ssl_get_current_cipher(self.ssl) + if c is None: + return None + return Cipher(c) + + def get_ciphers(self): + """Return an M2Crypto.SSL.Cipher_Stack object for this connection; if the + connection has not been initialised with cipher suites, return None.""" + c=m2.ssl_get_ciphers(self.ssl) + if c is None: + return None + return Cipher_Stack(c) + + def get_cipher_list(self, idx=0): + """Return the cipher suites for this connection as a string object.""" + return m2.ssl_get_cipher_list(self.ssl, idx) + + def set_cipher_list(self, cipher_list): + """Set the cipher suites for this connection.""" + return m2.ssl_set_cipher_list(self.ssl, cipher_list) + + def makefile(self, mode='rb', bufsize='ignored'): + r = 'r' in mode or '+' in mode + w = 'w' in mode or 'a' in mode or '+' in mode + b = 'b' in mode + m2mode = ['', 'r'][r] + ['', 'w'][w] + ['', 'b'][b] + # XXX Need to dup(). + bio = BIO.BIO(self.sslbio, _close_cb=self.close) + m2.bio_do_handshake(bio._ptr()) + return BIO.IOBuffer(bio, m2mode, _pyfree=0) + + def getsockname(self): + return self.socket.getsockname() + + def getpeername(self): + return self.socket.getpeername() + + def set_session_id_ctx(self, id): + ret = m2.ssl_set_session_id_context(self.ssl, id) + if not ret: + raise SSLError(m2.err_reason_error_string(m2.err_get_error())) + + def get_session(self): + sess = m2.ssl_get_session(self.ssl) + return Session(sess) + + def set_session(self, session): + m2.ssl_set_session(self.ssl, session._ptr()) + + def get_default_session_timeout(self): + return m2.ssl_get_default_session_timeout(self.ssl) + + def get_socket_read_timeout(self): + return timeout.struct_to_timeout(self.socket.getsockopt(socket.SOL_SOCKET, socket.SO_RCVTIMEO, timeout.struct_size())) + + def get_socket_write_timeout(self): + return timeout.struct_to_timeout(self.socket.getsockopt(socket.SOL_SOCKET, socket.SO_SNDTIMEO, timeout.struct_size())) + + def set_socket_read_timeout(self, timeo): + assert isinstance(timeo, timeout.timeout) + self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_RCVTIMEO, timeo.pack()) + + def set_socket_write_timeout(self, timeo): + assert isinstance(timeo, timeout.timeout) + self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_SNDTIMEO, timeo.pack()) + + def get_version(self): + "Return the TLS/SSL protocol version for this connection." + return m2.ssl_get_version(self.ssl) + + def set_post_connection_check_callback(self, postConnectionCheck): + self.postConnectionCheck = postConnectionCheck diff --git a/M2Crypto/SSL/Context.py b/M2Crypto/SSL/Context.py new file mode 100644 index 0000000..b8d159f --- /dev/null +++ b/M2Crypto/SSL/Context.py @@ -0,0 +1,254 @@ +"""SSL Context + +Copyright (c) 1999-2004 Ng Pheng Siong. All rights reserved.""" + +__all__ = ['map', 'Context'] + +from weakref import WeakValueDictionary + +# M2Crypto +import cb +from M2Crypto import util, BIO, Err, RSA, m2, X509 + +class _ctxmap: + singleton = None + def __init__(self): + self.map = WeakValueDictionary() + + def __getitem__(self, key): + return self.map[key] + + def __setitem__(self, key, value): + self.map[key] = value + + def __delitem__(self, key): + del self.map[key] + +def map(): + if _ctxmap.singleton is None: + _ctxmap.singleton = _ctxmap() + return _ctxmap.singleton + + +class Context: + + """'Context' for SSL connections.""" + + m2_ssl_ctx_free = m2.ssl_ctx_free + + def __init__(self, protocol='sslv23', weak_crypto=None): + proto = getattr(m2, protocol + '_method', None) + if proto is None: + raise ValueError, "no such protocol '%s'" % protocol + self.ctx = m2.ssl_ctx_new(proto()) + self.allow_unknown_ca = 0 + map()[long(self.ctx)] = self + m2.ssl_ctx_set_cache_size(self.ctx, 128L) + if weak_crypto is None: + if protocol == 'sslv23': + self.set_options(m2.SSL_OP_ALL | m2.SSL_OP_NO_SSLv2) + self.set_cipher_list('ALL:!ADH:!LOW:!EXP:!MD5:@STRENGTH') + + def __del__(self): + if getattr(self, 'ctx', None): + self.m2_ssl_ctx_free(self.ctx) + + def close(self): + del map()[long(self.ctx)] + + def load_cert(self, certfile, keyfile=None, callback=util.passphrase_callback): + """Load certificate and private key into the context. + + @param certfile: File that contains the PEM-encoded certificate. + @type certfile: str + @param keyfile: File that contains the PEM-encoded private key. + Default value of None indicates that the private key + is to be found in 'certfile'. + @type keyfile: str + + @param callback: Callable object to be invoked if the private key is + passphrase-protected. Default callback provides a + simple terminal-style input for the passphrase. + """ + m2.ssl_ctx_passphrase_callback(self.ctx, callback) + m2.ssl_ctx_use_cert(self.ctx, certfile) + if not keyfile: + keyfile = certfile + m2.ssl_ctx_use_privkey(self.ctx, keyfile) + if not m2.ssl_ctx_check_privkey(self.ctx): + raise ValueError, 'public/private key mismatch' + + def load_cert_chain(self, certchainfile, keyfile=None, callback=util.passphrase_callback): + """Load certificate chain and private key into the context. + + @param certchainfile: File object containing the PEM-encoded + certificate chain. + @type certchainfile: str + @param keyfile: File object containing the PEM-encoded private + key. Default value of None indicates that the + private key is to be found in 'certchainfile'. + @type keyfile: str + + @param callback: Callable object to be invoked if the private key + is passphrase-protected. Default callback + provides a simple terminal-style input for the + passphrase. + """ + m2.ssl_ctx_passphrase_callback(self.ctx, callback) + m2.ssl_ctx_use_cert_chain(self.ctx, certchainfile) + if not keyfile: + keyfile = certchainfile + m2.ssl_ctx_use_privkey(self.ctx, keyfile) + if not m2.ssl_ctx_check_privkey(self.ctx): + raise ValueError, 'public/private key mismatch' + + def set_client_CA_list_from_file(self, cafile): + """Load CA certs into the context. These CA certs are sent to the + peer during *SSLv3 certificate request*. + + @param cafile: File object containing one or more PEM-encoded CA + certificates concatenated together. + @type cafile: str + """ + m2.ssl_ctx_set_client_CA_list_from_file(self.ctx, cafile) + + # Deprecated. + load_client_CA = load_client_ca = set_client_CA_list_from_file + + def load_verify_locations(self, cafile=None, capath=None): + """Load CA certs into the context. These CA certs are used during + verification of the peer's certificate. + + @param cafile: File containing one or more PEM-encoded CA certificates + concatenated together. + @type cafile: str + @param capath: Directory containing PEM-encoded CA certificates + (one certificate per file). + @type capath: str + """ + if cafile is None and capath is None: + raise ValueError("cafile and capath can not both be None.") + return m2.ssl_ctx_load_verify_locations(self.ctx, cafile, capath) + + # Deprecated. + load_verify_info = load_verify_locations + + def set_session_id_ctx(self, id): + ret = m2.ssl_ctx_set_session_id_context(self.ctx, id) + if not ret: + raise Err.SSLError(Err.get_error_code(), '') + + def set_allow_unknown_ca(self, ok): + """Set the context to accept/reject a peer certificate if the + certificate's CA is unknown. + + @param ok: True to accept, False to reject. + @type ok: boolean + """ + self.allow_unknown_ca = ok + + def get_allow_unknown_ca(self): + """Get the context's setting that accepts/rejects a peer + certificate if the certificate's CA is unknown. + """ + return self.allow_unknown_ca + + def set_verify(self, mode, depth, callback=None): + """ + Set verify options. Most applications will need to call this + method with the right options to make a secure SSL connection. + + @param mode: The verification mode to use. Typically at least + SSL.verify_peer is used. Clients would also typically + add SSL.verify_fail_if_no_peer_cert. + @type mode: int + @param depth: The maximum allowed depth of the certificate chain + returned by the peer. + @type depth: int + @param callback: Callable that can be used to specify custom + verification checks. + """ + if callback is None: + m2.ssl_ctx_set_verify_default(self.ctx, mode) + else: + m2.ssl_ctx_set_verify(self.ctx, mode, callback) + m2.ssl_ctx_set_verify_depth(self.ctx, depth) + + def get_verify_mode(self): + return m2.ssl_ctx_get_verify_mode(self.ctx) + + def get_verify_depth(self): + return m2.ssl_ctx_get_verify_depth(self.ctx) + + def set_tmp_dh(self, dhpfile): + """Load ephemeral DH parameters into the context. + + @param dhpfile: File object containing the PEM-encoded DH + parameters. + @type dhpfile: str + """ + f = BIO.openfile(dhpfile) + dhp = m2.dh_read_parameters(f.bio_ptr()) + return m2.ssl_ctx_set_tmp_dh(self.ctx, dhp) + + def set_tmp_dh_callback(self, callback=None): + if callback is not None: + m2.ssl_ctx_set_tmp_dh_callback(self.ctx, callback) + + def set_tmp_rsa(self, rsa): + """Load ephemeral RSA key into the context. + + @param rsa: M2Crypto.RSA.RSA instance. + """ + if isinstance(rsa, RSA.RSA): + return m2.ssl_ctx_set_tmp_rsa(self.ctx, rsa.rsa) + else: + raise TypeError, "Expected an instance of RSA.RSA, got %s." % (rsa,) + + def set_tmp_rsa_callback(self, callback=None): + if callback is not None: + m2.ssl_ctx_set_tmp_rsa_callback(self.ctx, callback) + + def set_info_callback(self, callback=cb.ssl_info_callback): + """ + Set a callback function that can be used to get state information + about the SSL connections that are created from this context. + + @param callback: Callback function. The default prints information to + stderr. + """ + m2.ssl_ctx_set_info_callback(self.ctx, callback) + + def set_cipher_list(self, cipher_list): + return m2.ssl_ctx_set_cipher_list(self.ctx, cipher_list) + + def add_session(self, session): + return m2.ssl_ctx_add_session(self.ctx, session._ptr()) + + def remove_session(self, session): + return m2.ssl_ctx_remove_session(self.ctx, session._ptr()) + + def get_session_timeout(self): + return m2.ssl_ctx_get_session_timeout(self.ctx) + + def set_session_timeout(self, timeout): + return m2.ssl_ctx_set_session_timeout(self.ctx, timeout) + + def set_session_cache_mode(self, mode): + return m2.ssl_ctx_set_session_cache_mode(self.ctx, mode) + + def get_session_cache_mode(self): + return m2.ssl_ctx_get_session_cache_mode(self.ctx) + + def set_options(self, op): + return m2.ssl_ctx_set_options(self.ctx, op) + + def get_cert_store(self): + """ + Get the certificate store associated with this context. + + @warning: The store is NOT refcounted, and as such can not be relied + to be valid once the context goes away or is changed. + """ + return X509.X509_Store(m2.ssl_ctx_get_cert_store(self.ctx)) + diff --git a/M2Crypto/SSL/SSLServer.py b/M2Crypto/SSL/SSLServer.py new file mode 100644 index 0000000..d894d3b --- /dev/null +++ b/M2Crypto/SSL/SSLServer.py @@ -0,0 +1,53 @@ +"""SSLServer + +Copyright (c) 1999-2002 Ng Pheng Siong. All rights reserved.""" + +__all__ = ['SSLServer', 'ForkingSSLServer', 'ThreadingSSLServer'] + +# Python +import socket, SocketServer + +# M2Crypto +from Connection import Connection +from M2Crypto.SSL import SSLError +from M2Crypto import m2 + + +class SSLServer(SocketServer.TCPServer): + def __init__(self, server_address, RequestHandlerClass, ssl_context, bind_and_activate=True): + """ + Superclass says: Constructor. May be extended, do not override. + This class says: Ho-hum. + """ + SocketServer.BaseServer.__init__(self, server_address, RequestHandlerClass) + self.ssl_ctx=ssl_context + self.socket=Connection(self.ssl_ctx) + if bind_and_activate: + self.server_bind() + self.server_activate() + + def handle_request(self): + request = None + client_address = None + try: + request, client_address = self.get_request() + if self.verify_request(request, client_address): + self.process_request(request, client_address) + except SSLError: + self.handle_error(request, client_address) + + def handle_error(self, request, client_address): + print '-'*40 + import traceback + traceback.print_exc() + print '-'*40 + + +class ForkingSSLServer(SocketServer.ForkingMixIn, SSLServer): + pass + + +class ThreadingSSLServer(SocketServer.ThreadingMixIn, SSLServer): + pass + + diff --git a/M2Crypto/SSL/Session.py b/M2Crypto/SSL/Session.py new file mode 100644 index 0000000..1edf5b0 --- /dev/null +++ b/M2Crypto/SSL/Session.py @@ -0,0 +1,58 @@ +"""SSL Session + +Copyright (c) 1999-2003 Ng Pheng Siong. All rights reserved.""" + +__all__ = ['Session', 'load_session'] + +from M2Crypto import BIO, Err, m2 + +class Session: + + m2_ssl_session_free = m2.ssl_session_free + + def __init__(self, session, _pyfree=0): + assert session is not None + self.session = session + self._pyfree = _pyfree + + def __del__(self): + if getattr(self, '_pyfree', 0): + self.m2_ssl_session_free(self.session) + + def _ptr(self): + return self.session + + def as_text(self): + buf = BIO.MemoryBuffer() + m2.ssl_session_print(buf.bio_ptr(), self.session) + return buf.read_all() + + def as_der(self): + buf = BIO.MemoryBuffer() + m2.i2d_ssl_session(buf.bio_ptr(), self.session) + return buf.read_all() + + def write_bio(self, bio): + return m2.ssl_session_write_bio(bio.bio_ptr(), self.session) + + def get_time(self): + return m2.ssl_session_get_time(self.session) + + def set_time(self, t): + return m2.ssl_session_set_time(self.session, t) + + def get_timeout(self): + return m2.ssl_session_get_timeout(self.session) + + def set_timeout(self, t): + return m2.ssl_session_set_timeout(self.session, t) + + +def load_session(pemfile): + f = BIO.openfile(pemfile) + cptr = m2.ssl_session_read_pem(f.bio_ptr()) + f.close() + if cptr is None: + from M2Crypto.SSL import SSLError + raise SSLError(Err.get_error()) + return Session(cptr, 1) diff --git a/M2Crypto/SSL/TwistedProtocolWrapper.py b/M2Crypto/SSL/TwistedProtocolWrapper.py new file mode 100644 index 0000000..930dd32 --- /dev/null +++ b/M2Crypto/SSL/TwistedProtocolWrapper.py @@ -0,0 +1,410 @@ +""" +Make Twisted use M2Crypto for SSL + +Copyright (c) 2004-2007 Open Source Applications Foundation. +All rights reserved. +""" + +__all__ = ['connectSSL', 'connectTCP', 'listenSSL', 'listenTCP', + 'TLSProtocolWrapper'] + +import twisted.protocols.policies as policies +import twisted.internet.reactor +from twisted.protocols.policies import ProtocolWrapper +from twisted.internet.interfaces import ITLSTransport +from zope.interface import implements + +import M2Crypto # for M2Crypto.BIO.BIOError +from M2Crypto import m2, X509 +from M2Crypto.SSL import Checker + + +def _alwaysSucceedsPostConnectionCheck(peerX509, expectedHost): + return 1 + + +def connectSSL(host, port, factory, contextFactory, timeout=30, + bindAddress=None, + reactor=twisted.internet.reactor, + postConnectionCheck=Checker.Checker()): + """ + A convenience function to start an SSL/TLS connection using Twisted. + + See IReactorSSL interface in Twisted. + """ + wrappingFactory = policies.WrappingFactory(factory) + wrappingFactory.protocol = lambda factory, wrappedProtocol: \ + TLSProtocolWrapper(factory, + wrappedProtocol, + startPassThrough=0, + client=1, + contextFactory=contextFactory, + postConnectionCheck=postConnectionCheck) + return reactor.connectTCP(host, port, wrappingFactory, timeout, bindAddress) + + +def connectTCP(host, port, factory, timeout=30, bindAddress=None, + reactor=twisted.internet.reactor, + postConnectionCheck=Checker.Checker()): + """ + A convenience function to start a TCP connection using Twisted. + + NOTE: You must call startTLS(ctx) to go into SSL/TLS mode. + + See IReactorTCP interface in Twisted. + """ + wrappingFactory = policies.WrappingFactory(factory) + wrappingFactory.protocol = lambda factory, wrappedProtocol: \ + TLSProtocolWrapper(factory, + wrappedProtocol, + startPassThrough=1, + client=1, + contextFactory=None, + postConnectionCheck=postConnectionCheck) + return reactor.connectTCP(host, port, wrappingFactory, timeout, bindAddress) + + +def listenSSL(port, factory, contextFactory, backlog=5, interface='', + reactor=twisted.internet.reactor, + postConnectionCheck=_alwaysSucceedsPostConnectionCheck): + """ + A convenience function to listen for SSL/TLS connections using Twisted. + + See IReactorSSL interface in Twisted. + """ + wrappingFactory = policies.WrappingFactory(factory) + wrappingFactory.protocol = lambda factory, wrappedProtocol: \ + TLSProtocolWrapper(factory, + wrappedProtocol, + startPassThrough=0, + client=0, + contextFactory=contextFactory, + postConnectionCheck=postConnectionCheck) + return reactor.listenTCP(port, wrappingFactory, backlog, interface) + + +def listenTCP(port, factory, backlog=5, interface='', + reactor=twisted.internet.reactor, + postConnectionCheck=None): + """ + A convenience function to listen for TCP connections using Twisted. + + NOTE: You must call startTLS(ctx) to go into SSL/TLS mode. + + See IReactorTCP interface in Twisted. + """ + wrappingFactory = policies.WrappingFactory(factory) + wrappingFactory.protocol = lambda factory, wrappedProtocol: \ + TLSProtocolWrapper(factory, + wrappedProtocol, + startPassThrough=1, + client=0, + contextFactory=None, + postConnectionCheck=postConnectionCheck) + return reactor.listenTCP(port, wrappingFactory, backlog, interface) + + +class _BioProxy: + """ + The purpose of this class is to eliminate the __del__ method from + TLSProtocolWrapper, and thus letting it be garbage collected. + """ + + m2_bio_free_all = m2.bio_free_all + + def __init__(self, bio): + self.bio = bio + + def _ptr(self): + return self.bio + + def __del__(self): + if self.bio is not None: + self.m2_bio_free_all(self.bio) + + +class _SSLProxy: + """ + The purpose of this class is to eliminate the __del__ method from + TLSProtocolWrapper, and thus letting it be garbage collected. + """ + + m2_ssl_free = m2.ssl_free + + def __init__(self, ssl): + self.ssl = ssl + + def _ptr(self): + return self.ssl + + def __del__(self): + if self.ssl is not None: + self.m2_ssl_free(self.ssl) + + +class TLSProtocolWrapper(ProtocolWrapper): + """ + A SSL/TLS protocol wrapper to be used with Twisted. Typically + you would not use this class directly. Use connectTCP, + connectSSL, listenTCP, listenSSL functions defined above, + which will hook in this class. + """ + + implements(ITLSTransport) + + def __init__(self, factory, wrappedProtocol, startPassThrough, client, + contextFactory, postConnectionCheck): + """ + @param factory: + @param wrappedProtocol: + @param startPassThrough: If true we won't encrypt at all. Need to + call startTLS() later to switch to SSL/TLS. + @param client: True if this should be a client protocol. + @param contextFactory: Factory that creates SSL.Context objects. + The called function is getContext(). + @param postConnectionCheck: The post connection check callback that + will be called just after connection has + been established but before any real data + has been exchanged. The first argument to + this function is an X509 object, the second + is the expected host name string. + """ + #ProtocolWrapper.__init__(self, factory, wrappedProtocol) + #XXX: Twisted 2.0 has a new addition where the wrappingFactory is + # set as the factory of the wrappedProtocol. This is an issue + # as the wrap should be transparent. What we want is + # the factory of the wrappedProtocol to be the wrappedFactory and + # not the outer wrappingFactory. This is how it was implemented in + # Twisted 1.3 + self.factory = factory + self.wrappedProtocol = wrappedProtocol + + # wrappedProtocol == client/server instance + # factory.wrappedFactory == client/server factory + + self.data = '' # Clear text to encrypt and send + self.encrypted = '' # Encrypted data we need to decrypt and pass on + self.tlsStarted = 0 # SSL/TLS mode or pass through + self.checked = 0 # Post connection check done or not + self.isClient = client + self.helloDone = 0 # True when hello has been sent + if postConnectionCheck is None: + self.postConnectionCheck = _alwaysSucceedsPostConnectionCheck + else: + self.postConnectionCheck = postConnectionCheck + + if not startPassThrough: + self.startTLS(contextFactory.getContext()) + + def clear(self): + """ + Clear this instance, after which it is ready for reuse. + """ + if getattr(self, 'tlsStarted', 0): + self.sslBio = None + self.ssl = None + self.internalBio = None + self.networkBio = None + self.data = '' + self.encrypted = '' + self.tlsStarted = 0 + self.checked = 0 + self.isClient = 1 + self.helloDone = 0 + # We can reuse self.ctx and it will be deleted automatically + # when this instance dies + + def startTLS(self, ctx): + """ + Start SSL/TLS. If this is not called, this instance just passes data + through untouched. + """ + # NOTE: This method signature must match the startTLS() method Twisted + # expects transports to have. This will be called automatically + # by Twisted in STARTTLS situations, for example with SMTP. + if self.tlsStarted: + raise Exception, 'TLS already started' + + self.ctx = ctx + + self.internalBio = m2.bio_new(m2.bio_s_bio()) + m2.bio_set_write_buf_size(self.internalBio, 0) + self.networkBio = _BioProxy(m2.bio_new(m2.bio_s_bio())) + m2.bio_set_write_buf_size(self.networkBio._ptr(), 0) + m2.bio_make_bio_pair(self.internalBio, self.networkBio._ptr()) + + self.sslBio = _BioProxy(m2.bio_new(m2.bio_f_ssl())) + + self.ssl = _SSLProxy(m2.ssl_new(self.ctx.ctx)) + + if self.isClient: + m2.ssl_set_connect_state(self.ssl._ptr()) + else: + m2.ssl_set_accept_state(self.ssl._ptr()) + + m2.ssl_set_bio(self.ssl._ptr(), self.internalBio, self.internalBio) + m2.bio_set_ssl(self.sslBio._ptr(), self.ssl._ptr(), m2.bio_noclose) + + # Need this for writes that are larger than BIO pair buffers + mode = m2.ssl_get_mode(self.ssl._ptr()) + m2.ssl_set_mode(self.ssl._ptr(), + mode | + m2.SSL_MODE_ENABLE_PARTIAL_WRITE | + m2.SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER) + + self.tlsStarted = 1 + + def write(self, data): + if not self.tlsStarted: + ProtocolWrapper.write(self, data) + return + + try: + encryptedData = self._encrypt(data) + ProtocolWrapper.write(self, encryptedData) + self.helloDone = 1 + except M2Crypto.BIO.BIOError, e: + # See http://www.openssl.org/docs/apps/verify.html#DIAGNOSTICS + # for the error codes returned by SSL_get_verify_result. + e.args = (m2.ssl_get_verify_result(self.ssl._ptr()), e.args[0]) + raise e + + def writeSequence(self, data): + if not self.tlsStarted: + ProtocolWrapper.writeSequence(self, ''.join(data)) + return + + self.write(''.join(data)) + + def loseConnection(self): + # XXX Do we need to do m2.ssl_shutdown(self.ssl._ptr())? + ProtocolWrapper.loseConnection(self) + + def connectionMade(self): + ProtocolWrapper.connectionMade(self) + if self.tlsStarted and self.isClient and not self.helloDone: + self._clientHello() + + def dataReceived(self, data): + if not self.tlsStarted: + ProtocolWrapper.dataReceived(self, data) + return + + self.encrypted += data + + try: + while 1: + decryptedData = self._decrypt() + + self._check() + + encryptedData = self._encrypt() + ProtocolWrapper.write(self, encryptedData) + + ProtocolWrapper.dataReceived(self, decryptedData) + + if decryptedData == '' and encryptedData == '': + break + except M2Crypto.BIO.BIOError, e: + # See http://www.openssl.org/docs/apps/verify.html#DIAGNOSTICS + # for the error codes returned by SSL_get_verify_result. + e.args = (m2.ssl_get_verify_result(self.ssl._ptr()), e.args[0]) + raise e + + def connectionLost(self, reason): + self.clear() + ProtocolWrapper.connectionLost(self, reason) + + def _check(self): + if not self.checked and m2.ssl_is_init_finished(self.ssl._ptr()): + x509 = m2.ssl_get_peer_cert(self.ssl._ptr()) + if x509 is not None: + x509 = X509.X509(x509, 1) + if self.isClient: + host = self.transport.addr[0] + else: + host = self.transport.getPeer().host + if not self.postConnectionCheck(x509, host): + raise Checker.SSLVerificationError, 'post connection check' + self.checked = 1 + + def _clientHello(self): + try: + # We rely on OpenSSL implicitly starting with client hello + # when we haven't yet established an SSL connection + encryptedData = self._encrypt(clientHello=1) + ProtocolWrapper.write(self, encryptedData) + self.helloDone = 1 + except M2Crypto.BIO.BIOError, e: + # See http://www.openssl.org/docs/apps/verify.html#DIAGNOSTICS + # for the error codes returned by SSL_get_verify_result. + e.args = (m2.ssl_get_verify_result(self.ssl._ptr()), e.args[0]) + raise e + + def _encrypt(self, data='', clientHello=0): + # XXX near mirror image of _decrypt - refactor + encryptedData = '' + self.data += data + # Optimizations to reduce attribute accesses + sslBioPtr = self.sslBio._ptr() + networkBio = self.networkBio._ptr() + m2bio_ctrl_get_write_guarantee = m2.bio_ctrl_get_write_guarantee + m2bio_write = m2.bio_write + m2bio_should_retry = m2.bio_should_retry + m2bio_ctrl_pending = m2.bio_ctrl_pending + m2bio_read = m2.bio_read + + while 1: + g = m2bio_ctrl_get_write_guarantee(sslBioPtr) + if g > 0 and self.data != '' or clientHello: + r = m2bio_write(sslBioPtr, self.data) + if r <= 0: + assert(m2bio_should_retry(sslBioPtr)) + else: + assert(self.checked) + self.data = self.data[r:] + + pending = m2bio_ctrl_pending(networkBio) + if pending: + d = m2bio_read(networkBio, pending) + if d is not None: # This is strange, but d can be None + encryptedData += d + else: + assert(m2bio_should_retry(networkBio)) + else: + break + return encryptedData + + def _decrypt(self, data=''): + # XXX near mirror image of _encrypt - refactor + self.encrypted += data + decryptedData = '' + # Optimizations to reduce attribute accesses + sslBioPtr = self.sslBio._ptr() + networkBio = self.networkBio._ptr() + m2bio_ctrl_get_write_guarantee = m2.bio_ctrl_get_write_guarantee + m2bio_write = m2.bio_write + m2bio_should_retry = m2.bio_should_retry + m2bio_ctrl_pending = m2.bio_ctrl_pending + m2bio_read = m2.bio_read + + while 1: + g = m2bio_ctrl_get_write_guarantee(networkBio) + if g > 0 and self.encrypted != '': + r = m2bio_write(networkBio, self.encrypted) + if r <= 0: + assert(m2bio_should_retry(networkBio)) + else: + self.encrypted = self.encrypted[r:] + + pending = m2bio_ctrl_pending(sslBioPtr) + if pending: + d = m2bio_read(sslBioPtr, pending) + if d is not None: # This is strange, but d can be None + decryptedData += d + else: + assert(m2bio_should_retry(sslBioPtr)) + else: + break + + return decryptedData diff --git a/M2Crypto/SSL/__init__.py b/M2Crypto/SSL/__init__.py new file mode 100644 index 0000000..a013569 --- /dev/null +++ b/M2Crypto/SSL/__init__.py @@ -0,0 +1,28 @@ +"""M2Crypto SSL services. + +Copyright (c) 1999-2004 Ng Pheng Siong. All rights reserved.""" + +# M2Crypto +from M2Crypto import m2 + +class SSLError(Exception): pass +m2.ssl_init(SSLError) + +# M2Crypto.SSL +from Cipher import Cipher, Cipher_Stack +from Context import Context +from Connection import Connection +from SSLServer import SSLServer, ForkingSSLServer, ThreadingSSLServer +from ssl_dispatcher import ssl_dispatcher +from timeout import timeout + +verify_none = m2.SSL_VERIFY_NONE +verify_peer = m2.SSL_VERIFY_PEER +verify_fail_if_no_peer_cert = m2.SSL_VERIFY_FAIL_IF_NO_PEER_CERT +verify_client_once = m2.SSL_VERIFY_CLIENT_ONCE + +SSL_SENT_SHUTDOWN = m2.SSL_SENT_SHUTDOWN +SSL_RECEIVED_SHUTDOWN = m2.SSL_RECEIVED_SHUTDOWN + +op_all = m2.SSL_OP_ALL +op_no_sslv2 = m2.SSL_OP_NO_SSLv2 diff --git a/M2Crypto/SSL/cb.py b/M2Crypto/SSL/cb.py new file mode 100644 index 0000000..c1710cc --- /dev/null +++ b/M2Crypto/SSL/cb.py @@ -0,0 +1,83 @@ +"""SSL callbacks + +Copyright (c) 1999-2003 Ng Pheng Siong. All rights reserved.""" + +__all__ = ['unknown_issuer', 'ssl_verify_callback_stub', 'ssl_verify_callback', + 'ssl_verify_callback_allow_unknown_ca', 'ssl_info_callback'] + +# Python +import sys + +# M2Crypto +import Context +from M2Crypto import m2 + +def ssl_verify_callback_stub(ssl_ctx_ptr, x509_ptr, errnum, errdepth, ok): + # Deprecated + return ok + +unknown_issuer = [ + m2.X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT, + m2.X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY, + m2.X509_V_ERR_UNABLE_TO_VERIFY_LEAF_SIGNATURE, + m2.X509_V_ERR_CERT_UNTRUSTED, + ] + +def ssl_verify_callback(ssl_ctx_ptr, x509_ptr, errnum, errdepth, ok): + # Deprecated + ssl_ctx = Context.map()[long(ssl_ctx_ptr)] + if errnum in unknown_issuer: + if ssl_ctx.get_allow_unknown_ca(): + sys.stderr.write("policy: %s: permitted...\n" % (m2.x509_get_verify_error(errnum))) + sys.stderr.flush() + ok = 1 + # CRL checking goes here... + if ok: + if ssl_ctx.get_verify_depth() >= errdepth: + ok = 1 + else: + ok = 0 + return ok + +def ssl_verify_callback_allow_unknown_ca(ok, store): + errnum = store.get_error() + if errnum in unknown_issuer: + ok = 1 + return ok + +# Cribbed from OpenSSL's apps/s_cb.c. +def ssl_info_callback(where, ret, ssl_ptr): + + w = where & ~m2.SSL_ST_MASK + if (w & m2.SSL_ST_CONNECT): + state = "SSL connect" + elif (w & m2.SSL_ST_ACCEPT): + state = "SSL accept" + else: + state = "SSL state unknown" + + if (where & m2.SSL_CB_LOOP): + sys.stderr.write("LOOP: %s: %s\n" % (state, m2.ssl_get_state_v(ssl_ptr))) + sys.stderr.flush() + return + + if (where & m2.SSL_CB_EXIT): + if not ret: + sys.stderr.write("FAILED: %s: %s\n" % (state, m2.ssl_get_state_v(ssl_ptr))) + sys.stderr.flush() + else: + sys.stderr.write("INFO: %s: %s\n" % (state, m2.ssl_get_state_v(ssl_ptr))) + sys.stderr.flush() + return + + if (where & m2.SSL_CB_ALERT): + if (where & m2.SSL_CB_READ): + w = 'read' + else: + w = 'write' + sys.stderr.write("ALERT: %s: %s: %s\n" % \ + (w, m2.ssl_get_alert_type_v(ret), m2.ssl_get_alert_desc_v(ret))) + sys.stderr.flush() + return + + diff --git a/M2Crypto/SSL/ssl_dispatcher.py b/M2Crypto/SSL/ssl_dispatcher.py new file mode 100644 index 0000000..1e8c875 --- /dev/null +++ b/M2Crypto/SSL/ssl_dispatcher.py @@ -0,0 +1,36 @@ +"""SSL dispatcher + +Copyright (c) 1999-2002 Ng Pheng Siong. All rights reserved.""" + +__all__ = ['ssl_dispatcher'] + +# Python +import asyncore, socket + +# M2Crypto +from Connection import Connection +from M2Crypto import Err, m2 + + +class ssl_dispatcher(asyncore.dispatcher): + + def create_socket(self, ssl_context): + self.family_and_type=socket.AF_INET, socket.SOCK_STREAM + self.ssl_ctx=ssl_context + self.socket=Connection(self.ssl_ctx) + #self.socket.setblocking(0) + self.add_channel() + + def connect(self, addr): + self.socket.setblocking(1) + self.socket.connect(addr) + self.socket.setblocking(0) + + def recv(self, buffer_size=4096): + """Receive data over SSL.""" + return self.socket.recv(buffer_size) + + def send(self, buffer): + """Send data over SSL.""" + return self.socket.send(buffer) + diff --git a/M2Crypto/SSL/timeout.py b/M2Crypto/SSL/timeout.py new file mode 100644 index 0000000..d76556d --- /dev/null +++ b/M2Crypto/SSL/timeout.py @@ -0,0 +1,30 @@ +"""Support for SSL socket timeouts. + +Copyright (c) 1999-2003 Ng Pheng Siong. All rights reserved. + +Copyright 2008 Heikki Toivonen. All rights reserved. +""" + +__all__ = ['DEFAULT_TIMEOUT', 'timeout', 'struct_to_timeout', 'struct_size'] + +import struct +from M2Crypto import m2 + +DEFAULT_TIMEOUT = 600 + +class timeout: + + def __init__(self, sec=DEFAULT_TIMEOUT, microsec=0): + self.sec = sec + self.microsec = microsec + + def pack(self): + return struct.pack('ll', self.sec, self.microsec) + + +def struct_to_timeout(binstr): + (s, ms) = struct.unpack('ll', binstr) + return timeout(s, ms) + +def struct_size(): + return struct.calcsize('ll') diff --git a/M2Crypto/X509.py b/M2Crypto/X509.py new file mode 100644 index 0000000..eef83fe --- /dev/null +++ b/M2Crypto/X509.py @@ -0,0 +1,1104 @@ +"""M2Crypto wrapper for OpenSSL X509 API. + +Copyright (c) 1999-2004 Ng Pheng Siong. All rights reserved. + +Portions created by Open Source Applications Foundation (OSAF) are +Copyright (C) 2004-2007 OSAF. All Rights Reserved. +Author: Heikki Toivonen +""" + +# M2Crypto +from M2Crypto import ASN1, BIO, Err, EVP, util +import m2 + +FORMAT_DER = 0 +FORMAT_PEM = 1 + +class X509Error(Exception): pass + +m2.x509_init(X509Error) + +V_OK = m2.X509_V_OK + +def new_extension(name, value, critical=0, _pyfree=1): + """ + Create new X509_Extension instance. + """ + if name == 'subjectKeyIdentifier' and \ + value.strip('0123456789abcdefABCDEF:') is not '': + raise ValueError('value must be precomputed hash') + lhash = m2.x509v3_lhash() + ctx = m2.x509v3_set_conf_lhash(lhash) + x509_ext_ptr = m2.x509v3_ext_conf(lhash, ctx, name, value) + x509_ext = X509_Extension(x509_ext_ptr, _pyfree) + x509_ext.set_critical(critical) + return x509_ext + + +class X509_Extension: + """ + X509 Extension + """ + + m2_x509_extension_free = m2.x509_extension_free + + def __init__(self, x509_ext_ptr=None, _pyfree=1): + self.x509_ext = x509_ext_ptr + self._pyfree = _pyfree + + def __del__(self): + if getattr(self, '_pyfree', 0) and self.x509_ext: + self.m2_x509_extension_free(self.x509_ext) + + def _ptr(self): + return self.x509_ext + + def set_critical(self, critical=1): + """ + Mark this extension critical or noncritical. By default an + extension is not critical. + + @type critical: int + @param critical: Nonzero sets this extension as critical. + Calling this method without arguments will + set this extension to critical. + """ + return m2.x509_extension_set_critical(self.x509_ext, critical) + + def get_critical(self): + """ + Return whether or not this is a critical extension. + + @rtype: int + @return: Nonzero if this is a critical extension. + """ + return m2.x509_extension_get_critical(self.x509_ext) + + def get_name(self): + """ + Get the extension name, for example 'subjectAltName'. + """ + return m2.x509_extension_get_name(self.x509_ext) + + def get_value(self, flag=0, indent=0): + """ + Get the extension value, for example 'DNS:www.example.com'. + + @param flag: Flag to control what and how to print. + @param indent: How many spaces to print before actual value. + """ + buf=BIO.MemoryBuffer() + m2.x509_ext_print(buf.bio_ptr(), self.x509_ext, flag, indent) + return buf.read_all() + + +class X509_Extension_Stack: + """ + X509 Extension Stack + + @warning: Do not modify the underlying OpenSSL stack + except through this interface, or use any OpenSSL functions that do so + indirectly. Doing so will get the OpenSSL stack and the internal pystack + of this class out of sync, leading to python memory leaks, exceptions + or even python crashes! + """ + + m2_sk_x509_extension_free = m2.sk_x509_extension_free + + def __init__(self, stack=None, _pyfree=0): + if stack is not None: + self.stack = stack + self._pyfree = _pyfree + num = m2.sk_x509_extension_num(self.stack) + for i in range(num): + self.pystack.append(X509_Extension(m2.sk_x509_extension_value(self.stack, i), + _pyfree=_pyfree)) + else: + self.stack = m2.sk_x509_extension_new_null() + self._pyfree = 1 + self.pystack = [] # This must be kept in sync with self.stack + + def __del__(self): + if getattr(self, '_pyfree', 0): + self.m2_sk_x509_extension_free(self.stack) + + def __len__(self): + assert m2.sk_x509_extension_num(self.stack) == len(self.pystack) + return len(self.pystack) + + def __getitem__(self, idx): + return self.pystack[idx] + + def __iter__(self): + return iter(self.pystack) + + def _ptr(self): + return self.stack + + def push(self, x509_ext): + """ + Push X509_Extension object onto the stack. + + @type x509_ext: M2Crypto.X509.X509_Extension + @param x509_ext: X509_Extension object to be pushed onto the stack. + @return: The number of extensions on the stack. + """ + self.pystack.append(x509_ext) + ret = m2.sk_x509_extension_push(self.stack, x509_ext._ptr()) + assert ret == len(self.pystack) + return ret + + def pop(self): + """ + Pop X509_Extension object from the stack. + + @return: X509_Extension popped + """ + x509_ext_ptr = m2.sk_x509_extension_pop(self.stack) + if x509_ext_ptr is None: + assert len(self.pystack) == 0 + return None + return self.pystack.pop() + + +class X509_Name_Entry: + """ + X509 Name Entry + """ + + m2_x509_name_entry_free = m2.x509_name_entry_free + + def __init__(self, x509_name_entry, _pyfree=0): + self.x509_name_entry = x509_name_entry + self._pyfree = _pyfree + + def __del__(self): + if getattr(self, '_pyfree', 0): + self.m2_x509_name_entry_free(self.x509_name_entry) + + def _ptr(self): + return self.x509_name_entry + + def set_object(self, asn1obj): + return m2.x509_name_entry_set_object(self.x509_name_entry, + asn1obj._ptr()) + + def set_data(self, data, type=ASN1.MBSTRING_ASC): + return m2.x509_name_entry_set_data(self.x509_name_entry, + type, data) + + def get_object(self): + return ASN1.ASN1_Object(m2.x509_name_entry_get_object(self.x509_name_entry)) + + def get_data(self): + return ASN1.ASN1_String(m2.x509_name_entry_get_data(self.x509_name_entry)) + + def create_by_txt( self, field, type, entry, len): + return m2.x509_name_entry_create_by_txt(self.x509_name_entry._ptr(), + field, type, entry, len) + + +class X509_Name: + """ + X509 Name + """ + + nid = {'C' : m2.NID_countryName, + 'SP' : m2.NID_stateOrProvinceName, + 'ST' : m2.NID_stateOrProvinceName, + 'stateOrProvinceName' : m2.NID_stateOrProvinceName, + 'L' : m2.NID_localityName, + 'localityName' : m2.NID_localityName, + 'O' : m2.NID_organizationName, + 'organizationName' : m2.NID_organizationName, + 'OU' : m2.NID_organizationalUnitName, + 'organizationUnitName' : m2.NID_organizationalUnitName, + 'CN' : m2.NID_commonName, + 'commonName' : m2.NID_commonName, + 'Email' : m2.NID_pkcs9_emailAddress, + 'emailAddress' : m2.NID_pkcs9_emailAddress, + 'serialNumber' : m2.NID_serialNumber, + 'SN' : m2.NID_surname, + 'surname' : m2.NID_surname, + 'GN' : m2.NID_givenName, + 'givenName' : m2.NID_givenName + } + + m2_x509_name_free = m2.x509_name_free + + def __init__(self, x509_name=None, _pyfree=0): + if x509_name is not None: + assert m2.x509_name_type_check(x509_name), "'x509_name' type error" + self.x509_name = x509_name + self._pyfree = _pyfree + else: + self.x509_name = m2.x509_name_new () + self._pyfree = 1 + + def __del__(self): + if getattr(self, '_pyfree', 0): + self.m2_x509_name_free(self.x509_name) + + def __str__(self): + assert m2.x509_name_type_check(self.x509_name), "'x509_name' type error" + return m2.x509_name_oneline(self.x509_name) + + def __getattr__(self, attr): + if attr in self.nid: + assert m2.x509_name_type_check(self.x509_name), "'x509_name' type error" + return m2.x509_name_by_nid(self.x509_name, self.nid[attr]) + + if attr in self.__dict__: + return self.__dict__[attr] + + raise AttributeError, (self, attr) + + def __setattr__(self, attr, value): + if attr in self.nid: + assert m2.x509_name_type_check(self.x509_name), "'x509_name' type error" + return m2.x509_name_set_by_nid(self.x509_name, self.nid[attr], value) + + self.__dict__[attr] = value + + def __len__(self): + return m2.x509_name_entry_count(self.x509_name) + + def __getitem__(self, idx): + if not 0 <= idx < self.entry_count(): + raise IndexError("index out of range") + return X509_Name_Entry(m2.x509_name_get_entry(self.x509_name, idx)) + + def __iter__(self): + for i in xrange(self.entry_count()): + yield self[i] + + def _ptr(self): + #assert m2.x509_name_type_check(self.x509_name), "'x509_name' type error" + return self.x509_name + + def add_entry_by_txt(self, field, type, entry, len, loc, set): + return m2.x509_name_add_entry_by_txt(self.x509_name, field, type, + entry, len, loc, set ) + + def entry_count( self ): + return m2.x509_name_entry_count( self.x509_name ) + + def get_entries_by_nid(self, nid): + ret = [] + lastpos = -1 + + while True: + lastpos = m2.x509_name_get_index_by_nid(self.x509_name, nid, + lastpos) + if lastpos == -1: + break + + ret.append(self[lastpos]) + + return ret + + def as_text(self, indent=0, flags=m2.XN_FLAG_COMPAT): + """ + as_text returns the name as a string. + + @param indent: Each line in multiline format is indented + by this many spaces. + @param flags: Flags that control how the output should be formatted. + """ + assert m2.x509_name_type_check(self.x509_name), "'x509_name' type error" + buf=BIO.MemoryBuffer() + m2.x509_name_print_ex(buf.bio_ptr(), self.x509_name, indent, flags) + return buf.read_all() + + def as_der(self): + assert m2.x509_name_type_check(self.x509_name), "'x509_name' type error" + return m2.x509_name_get_der(self.x509_name) + + def as_hash(self): + assert m2.x509_name_type_check(self.x509_name), "'x509_name' type error" + return m2.x509_name_hash(self.x509_name) + +class X509: + """ + X.509 Certificate + """ + + m2_x509_free = m2.x509_free + + def __init__(self, x509=None, _pyfree=0): + if x509 is not None: + assert m2.x509_type_check(x509), "'x509' type error" + self.x509 = x509 + self._pyfree = _pyfree + else: + self.x509 = m2.x509_new () + self._pyfree = 1 + + def __del__(self): + if getattr(self, '_pyfree', 0): + self.m2_x509_free(self.x509) + + def _ptr(self): + assert m2.x509_type_check(self.x509), "'x509' type error" + return self.x509 + + def as_text(self): + assert m2.x509_type_check(self.x509), "'x509' type error" + buf=BIO.MemoryBuffer() + m2.x509_print(buf.bio_ptr(), self.x509) + return buf.read_all() + + def as_der(self): + assert m2.x509_type_check(self.x509), "'x509' type error" + return m2.i2d_x509(self.x509) + + def as_pem(self): + buf=BIO.MemoryBuffer() + m2.x509_write_pem(buf.bio_ptr(), self.x509) + return buf.read_all() + + def save_pem(self, filename): + """ + save_pem + """ + bio=BIO.openfile(filename, 'wb') + return m2.x509_write_pem(bio.bio_ptr(), self.x509) + + def save(self, filename, format=FORMAT_PEM): + """ + Saves X.509 certificate to a file. Default output + format is PEM. + + @type filename: string + @param filename: Name of the file the cert will be saved to. + @type format: int + @param format: Controls what output format is used to save the cert. + Either FORMAT_PEM or FORMAT_DER to save in PEM or DER format. + Raises a ValueError if an unknow format is used. + """ + bio = BIO.openfile(filename, 'wb') + if format == FORMAT_PEM: + return m2.x509_write_pem(bio.bio_ptr(), self.x509) + elif format == FORMAT_DER: + return m2.i2d_x509_bio(bio.bio_ptr(), self.x509) + else: + raise ValueError("Unknown filetype. Must be either FORMAT_PEM or FORMAT_DER") + + def set_version(self, version): + """ + Set version. + + @type version: int + @param version: Version number. + @rtype: int + @return: Returns 0 on failure. + """ + assert m2.x509_type_check(self.x509), "'x509' type error" + return m2.x509_set_version(self.x509, version) + + def set_not_before(self, asn1_utctime): + assert m2.x509_type_check(self.x509), "'x509' type error" + return m2.x509_set_not_before(self.x509, asn1_utctime._ptr()) + + def set_not_after(self, asn1_utctime): + assert m2.x509_type_check(self.x509), "'x509' type error" + return m2.x509_set_not_after(self.x509, asn1_utctime._ptr()) + + def set_subject_name(self, name): + assert m2.x509_type_check(self.x509), "'x509' type error" + return m2.x509_set_subject_name(self.x509, name.x509_name) + + def set_issuer_name(self, name): + assert m2.x509_type_check(self.x509), "'x509' type error" + return m2.x509_set_issuer_name(self.x509, name.x509_name) + + def get_version(self): + assert m2.x509_type_check(self.x509), "'x509' type error" + return m2.x509_get_version(self.x509) + + def get_serial_number(self): + assert m2.x509_type_check(self.x509), "'x509' type error" + asn1_integer = m2.x509_get_serial_number(self.x509) + return m2.asn1_integer_get(asn1_integer) + + def set_serial_number(self, serial): + """ + Set serial number. + + @type serial: int + @param serial: Serial number. + """ + assert m2.x509_type_check(self.x509), "'x509' type error" + # This "magically" changes serial since asn1_integer + # is C pointer to x509's internal serial number. + asn1_integer = m2.x509_get_serial_number(self.x509) + return m2.asn1_integer_set(asn1_integer, serial) + # XXX Or should I do this? + #asn1_integer = m2.asn1_integer_new() + #m2.asn1_integer_set(asn1_integer, serial) + #return m2.x509_set_serial_number(self.x509, asn1_integer) + + def get_not_before(self): + assert m2.x509_type_check(self.x509), "'x509' type error" + return ASN1.ASN1_UTCTIME(m2.x509_get_not_before(self.x509)) + + def get_not_after(self): + assert m2.x509_type_check(self.x509), "'x509' type error" + return ASN1.ASN1_UTCTIME(m2.x509_get_not_after(self.x509)) + + def get_pubkey(self): + assert m2.x509_type_check(self.x509), "'x509' type error" + return EVP.PKey(m2.x509_get_pubkey(self.x509), _pyfree=1) + + def set_pubkey(self, pkey): + """ + Set the public key for the certificate + + @type pkey: EVP_PKEY + @param pkey: Public key + """ + assert m2.x509_type_check(self.x509), "'x509' type error" + return m2.x509_set_pubkey(self.x509, pkey.pkey) + + def get_issuer(self): + assert m2.x509_type_check(self.x509), "'x509' type error" + return X509_Name(m2.x509_get_issuer_name(self.x509)) + + def set_issuer(self, name): + """ + Set issuer name. + + @type name: X509_Name + @param name: subjectName field. + """ + assert m2.x509_type_check(self.x509), "'x509' type error" + return m2.x509_set_issuer_name(self.x509, name.x509_name) + + def get_subject(self): + assert m2.x509_type_check(self.x509), "'x509' type error" + return X509_Name(m2.x509_get_subject_name(self.x509)) + + def set_subject(self, name): + """ + Set subject name. + + @type name: X509_Name + @param name: subjectName field. + """ + assert m2.x509_type_check(self.x509), "'x509' type error" + return m2.x509_set_subject_name(self.x509, name.x509_name) + + def add_ext(self, ext): + """ + Add X509 extension to this certificate. + + @type ext: X509_Extension + @param ext: Extension + """ + assert m2.x509_type_check(self.x509), "'x509' type error" + return m2.x509_add_ext(self.x509, ext.x509_ext, -1) + + def get_ext(self, name): + """ + Get X509 extension by name. + + @type name: Name of the extension + @param name: str + @return: X509_Extension + """ + # Optimizations to reduce attribute accesses + m2x509_get_ext = m2.x509_get_ext + m2x509_extension_get_name = m2.x509_extension_get_name + x509 = self.x509 + + for i in range(m2.x509_get_ext_count(x509)): + extPtr = m2x509_get_ext(x509, i) + if m2x509_extension_get_name(extPtr) == name: + return X509_Extension(extPtr, _pyfree=0) + + raise LookupError + + def get_ext_at(self, index): + """ + Get X509 extension by index. + + @type index: Name of the extension + @param index: int + @return: X509_Extension + """ + if index < 0 or index >= self.get_ext_count(): + raise IndexError + + return X509_Extension(m2.x509_get_ext(self.x509, index), + _pyfree=0) + + def get_ext_count(self): + """ + Get X509 extension count. + """ + return m2.x509_get_ext_count(self.x509) + + def sign(self, pkey, md): + """ + Sign the certificate. + + @type pkey: EVP_PKEY + @param pkey: Public key + @type md: str + @param md: Message digest algorithm to use for signing, + for example 'sha1'. + """ + assert m2.x509_type_check(self.x509), "'x509' type error" + mda = getattr(m2, md, None) + if mda is None: + raise ValueError, ('unknown message digest', md) + return m2.x509_sign(self.x509, pkey.pkey, mda()) + + def verify(self, pkey=None): + assert m2.x509_type_check(self.x509), "'x509' type error" + if pkey: + return m2.x509_verify(self.x509, pkey.pkey) + else: + return m2.x509_verify(self.x509, self.get_pubkey().pkey) + + def check_ca(self): + """ + Check if the certificate is a Certificate Authority (CA) certificate. + + @return: 0 if the certificate is not CA, nonzero otherwise. + + @requires: OpenSSL 0.9.8 or newer + """ + return m2.x509_check_ca(self.x509) + + def check_purpose(self, id, ca): + """ + Check if the certificate's purpose matches the asked purpose. + + @param id: Purpose id. See X509_PURPOSE_* constants. + @param ca: 1 if the certificate should be CA, 0 otherwise. + @return: 0 if the certificate purpose does not match, nonzero otherwise. + """ + return m2.x509_check_purpose(self.x509, id, ca) + + def get_fingerprint(self, md='md5'): + """ + Get the fingerprint of the certificate. + + @param md: Message digest algorithm to use. + @return: String containing the fingerprint in hex format. + """ + der = self.as_der() + md = EVP.MessageDigest(md) + md.update(der) + digest = md.final() + return hex(util.octx_to_num(digest))[2:-1].upper() + +def load_cert(file, format=FORMAT_PEM): + """ + Load certificate from file. + + @type file: string + @param file: Name of file containing certificate in either DER or PEM format. + @type format: int, either FORMAT_PEM or FORMAT_DER + @param format: Describes the format of the file to be loaded, either PEM or DER. + + @rtype: M2Crypto.X509.X509 + @return: M2Crypto.X509.X509 object. + """ + bio = BIO.openfile(file) + if format == FORMAT_PEM: + return load_cert_bio(bio) + elif format == FORMAT_DER: + cptr = m2.d2i_x509(bio._ptr()) + if cptr is None: + raise X509Error(Err.get_error()) + return X509(cptr, _pyfree=1) + else: + raise ValueError("Unknown format. Must be either FORMAT_DER or FORMAT_PEM") + +def load_cert_bio(bio, format=FORMAT_PEM): + """ + Load certificate from a bio. + + @type bio: M2Crypto.BIO.BIO + @param bio: BIO pointing at a certificate in either DER or PEM format. + @type format: int, either FORMAT_PEM or FORMAT_DER + @param format: Describes the format of the cert to be loaded, either PEM or DER. + + @rtype: M2Crypto.X509.X509 + @return: M2Crypto.X509.X509 object. + """ + if format == FORMAT_PEM: + cptr = m2.x509_read_pem(bio._ptr()) + elif format == FORMAT_DER: + cptr = m2.d2i_x509(bio._ptr()) + else: + raise ValueError("Unknown format. Must be either FORMAT_DER or FORMAT_PEM") + if cptr is None: + raise X509Error(Err.get_error()) + return X509(cptr, _pyfree=1) + +def load_cert_string(string, format=FORMAT_PEM): + """ + Load certificate from a string. + + @type string: string + @param string: String containing a certificate in either DER or PEM format. + @type format: int, either FORMAT_PEM or FORMAT_DER + @param format: Describes the format of the cert to be loaded, either PEM or DER. + + @rtype: M2Crypto.X509.X509 + @return: M2Crypto.X509.X509 object. + """ + bio = BIO.MemoryBuffer(string) + return load_cert_bio(bio, format) + +def load_cert_der_string(string): + """ + Load certificate from a string. + + @type string: string + @param string: String containing a certificate in DER format. + + @rtype: M2Crypto.X509.X509 + @return: M2Crypto.X509.X509 object. + """ + bio = BIO.MemoryBuffer(string) + cptr = m2.d2i_x509(bio._ptr()) + if cptr is None: + raise X509Error(Err.get_error()) + return X509(cptr, _pyfree=1) + +class X509_Store_Context: + """ + X509 Store Context + """ + + m2_x509_store_ctx_free = m2.x509_store_ctx_free + + def __init__(self, x509_store_ctx, _pyfree=0): + self.ctx = x509_store_ctx + self._pyfree = _pyfree + + def __del__(self): + if self._pyfree: + self.m2_x509_store_ctx_free(self.ctx) + + def _ptr(self): + return self.ctx + + def get_current_cert(self): + """ + Get current X.509 certificate. + + @warning: The returned certificate is NOT refcounted, so you can not + rely on it being valid once the store context goes away or is modified. + """ + return X509(m2.x509_store_ctx_get_current_cert(self.ctx), _pyfree=0) + + def get_error(self): + """ + Get error code. + """ + return m2.x509_store_ctx_get_error(self.ctx) + + def get_error_depth(self): + """ + Get error depth. + """ + return m2.x509_store_ctx_get_error_depth(self.ctx) + + def get1_chain(self): + """ + Get certificate chain. + + @return: Reference counted (i.e. safe to use even after the store + context goes away) stack of certificates in the chain. + @rtype: X509_Stack + """ + return X509_Stack(m2.x509_store_ctx_get1_chain(self.ctx), 1, 1) + + +class X509_Store: + """ + X509 Store + """ + + m2_x509_store_free = m2.x509_store_free + + def __init__(self, store=None, _pyfree=0): + if store is not None: + self.store = store + self._pyfree = _pyfree + else: + self.store = m2.x509_store_new() + self._pyfree = 1 + + def __del__(self): + if getattr(self, '_pyfree', 0): + self.m2_x509_store_free(self.store) + + def _ptr(self): + return self.store + + def load_info(self, file): + ret = m2.x509_store_load_locations(self.store, file) + if ret < 1: + raise X509Error(Err.get_error()) + return ret + + load_locations = load_info + + def add_x509(self, x509): + assert isinstance(x509, X509) + return m2.x509_store_add_cert(self.store, x509._ptr()) + + add_cert = add_x509 + + +class X509_Stack: + """ + X509 Stack + + @warning: Do not modify the underlying OpenSSL stack + except through this interface, or use any OpenSSL functions that do so + indirectly. Doing so will get the OpenSSL stack and the internal pystack + of this class out of sync, leading to python memory leaks, exceptions + or even python crashes! + """ + + m2_sk_x509_free = m2.sk_x509_free + + def __init__(self, stack=None, _pyfree=0, _pyfree_x509=0): + if stack is not None: + self.stack = stack + self._pyfree = _pyfree + self.pystack = [] # This must be kept in sync with self.stack + num = m2.sk_x509_num(self.stack) + for i in range(num): + self.pystack.append(X509(m2.sk_x509_value(self.stack, i), + _pyfree=_pyfree_x509)) + else: + self.stack = m2.sk_x509_new_null() + self._pyfree = 1 + self.pystack = [] # This must be kept in sync with self.stack + + def __del__(self): + if getattr(self, '_pyfree', 0): + self.m2_sk_x509_free(self.stack) + + def __len__(self): + assert m2.sk_x509_num(self.stack) == len(self.pystack) + return len(self.pystack) + + def __getitem__(self, idx): + return self.pystack[idx] + + def __iter__(self): + return iter(self.pystack) + + def _ptr(self): + return self.stack + + def push(self, x509): + """ + push an X509 certificate onto the stack. + + @param x509: X509 object. + @return: The number of X509 objects currently on the stack. + """ + assert isinstance(x509, X509) + self.pystack.append(x509) + ret = m2.sk_x509_push(self.stack, x509._ptr()) + assert ret == len(self.pystack) + return ret + + def pop(self): + """ + pop a certificate from the stack. + + @return: X509 object that was popped, or None if there is nothing + to pop. + """ + x509_ptr = m2.sk_x509_pop(self.stack) + if x509_ptr is None: + assert len(self.pystack) == 0 + return None + return self.pystack.pop() + + def as_der(self): + """ + Return the stack as a DER encoded string + """ + return m2.get_der_encoding_stack(self.stack) + + +def new_stack_from_der(der_string): + """ + Create a new X509_Stack from DER string. + + @return: X509_Stack + """ + stack_ptr = m2.make_stack_from_der_sequence(der_string) + if stack_ptr is None: + raise X509Error(Err.get_error()) + return X509_Stack(stack_ptr, 1, 1) + + +class Request: + """ + X509 Certificate Request. + """ + + m2_x509_req_free = m2.x509_req_free + + def __init__(self, req=None, _pyfree=0): + if req is not None: + self.req = req + self._pyfree = _pyfree + else: + self.req = m2.x509_req_new() + self._pyfree = 1 + + def __del__(self): + if getattr(self, '_pyfree', 0): + self.m2_x509_req_free(self.req) + + def as_text(self): + buf=BIO.MemoryBuffer() + m2.x509_req_print(buf.bio_ptr(), self.req) + return buf.read_all() + + def as_pem(self): + buf=BIO.MemoryBuffer() + m2.x509_req_write_pem(buf.bio_ptr(), self.req) + return buf.read_all() + + def as_der(self): + buf = BIO.MemoryBuffer() + m2.i2d_x509_req_bio(buf.bio_ptr(), self.req) + return buf.read_all() + + def save_pem(self, filename): + bio=BIO.openfile(filename, 'wb') + return m2.x509_req_write_pem(bio.bio_ptr(), self.req) + + def save(self, filename, format=FORMAT_PEM): + """ + Saves X.509 certificate request to a file. Default output + format is PEM. + + @type filename: string + @param filename: Name of the file the request will be saved to. + @type format: int + @param format: Controls what output format is used to save the request. + Either FORMAT_PEM or FORMAT_DER to save in PEM or DER format. + Raises ValueError if an unknown format is used. + """ + bio = BIO.openfile(filename, 'wb') + if format == FORMAT_PEM: + return m2.x509_req_write_pem(bio.bio_ptr(), self.req) + elif format == FORMAT_DER: + return m2.i2d_x509_req_bio(bio.bio_ptr(), self.req) + else: + raise ValueError("Unknown filetype. Must be either FORMAT_DER or FORMAT_PEM") + + def get_pubkey(self): + """ + Get the public key for the request. + + @rtype: EVP_PKEY + @return: Public key from the request. + """ + return EVP.PKey(m2.x509_req_get_pubkey(self.req), _pyfree=1) + + def set_pubkey(self, pkey): + """ + Set the public key for the request. + + @type pkey: EVP_PKEY + @param pkey: Public key + + @rtype: int + @return: Return 1 for success and 0 for failure. + """ + return m2.x509_req_set_pubkey( self.req, pkey.pkey ) + + def get_version(self): + """ + Get version. + + @rtype: int + @return: Returns version. + """ + return m2.x509_req_get_version(self.req) + + def set_version(self, version): + """ + Set version. + + @type version: int + @param version: Version number. + @rtype: int + @return: Returns 0 on failure. + """ + return m2.x509_req_set_version( self.req, version ) + + def get_subject(self): + return X509_Name(m2.x509_req_get_subject_name( self.req )) + + def set_subject_name(self, name): + """ + Set subject name. + + @type name: X509_Name + @param name: subjectName field. + """ + return m2.x509_req_set_subject_name( self.req, name.x509_name ) + + set_subject = set_subject_name + + def add_extensions(self, ext_stack): + """ + Add X509 extensions to this request. + + @type ext_stack: X509_Extension_Stack + @param ext_stack: Stack of extensions to add. + """ + return m2.x509_req_add_extensions(self.req, ext_stack._ptr()) + + def verify(self, pkey): + return m2.x509_req_verify(self.req, pkey.pkey) + + def sign(self, pkey, md): + mda = getattr(m2, md, None) + if mda is None: + raise ValueError, ('unknown message digest', md) + return m2.x509_req_sign(self.req, pkey.pkey, mda()) + + +def load_request(file, format=FORMAT_PEM): + """ + Load certificate request from file. + + @type file: string + @param file: Name of file containing certificate request in either PEM or DER format. + @type format: int, either FORMAT_PEM or FORMAT_DER + @param format: Describes the format of the file to be loaded, either PEM or DER. + + @rtype: M2Crypto.X509.Request + @return: M2Crypto.X509.Request object. + """ + f=BIO.openfile(file) + if format == FORMAT_PEM: + cptr=m2.x509_req_read_pem(f.bio_ptr()) + elif format == FORMAT_DER: + cptr = m2.d2i_x509_req(f.bio_ptr()) + else: + raise ValueError("Unknown filetype. Must be either FORMAT_PEM or FORMAT_DER") + f.close() + if cptr is None: + raise X509Error(Err.get_error()) + return Request(cptr, 1) + +def load_request_bio(bio, format=FORMAT_PEM): + """ + Load certificate request from a bio. + + @type bio: M2Crypto.BIO.BIO + @param bio: BIO pointing at a certificate request in either DER or PEM format. + @type format: int, either FORMAT_PEM or FORMAT_DER + @param format: Describes the format of the request to be loaded, either PEM or DER. + + @rtype: M2Crypto.X509.Request + @return: M2Crypto.X509.Request object. + """ + if format == FORMAT_PEM: + cptr = m2.x509_req_read_pem(bio._ptr()) + elif format == FORMAT_DER: + cptr = m2.d2i_x509_req(bio._ptr()) + else: + raise ValueError("Unknown format. Must be either FORMAT_DER or FORMAT_PEM") + if cptr is None: + raise X509Error(Err.get_error()) + return Request(cptr, _pyfree=1) + +def load_request_string(string, format=FORMAT_PEM): + """ + Load certificate request from a string. + + @type string: string + @param string: String containing a certificate request in either DER or PEM format. + @type format: int, either FORMAT_PEM or FORMAT_DER + @param format: Describes the format of the request to be loaded, either PEM or DER. + + @rtype: M2Crypto.X509.Request + @return: M2Crypto.X509.Request object. + """ + bio = BIO.MemoryBuffer(string) + return load_request_bio(bio, format) + +def load_request_der_string(string): + """ + Load certificate request from a string. + + @type string: string + @param string: String containing a certificate request in DER format. + + @rtype: M2Crypto.X509.Request + @return: M2Crypto.X509.Request object. + """ + bio = BIO.MemoryBuffer(string) + return load_request_bio(bio, FORMAT_DER) + + +class CRL: + """ + X509 Certificate Revocation List + """ + + m2_x509_crl_free = m2.x509_crl_free + + def __init__(self, crl=None, _pyfree=0): + if crl is not None: + self.crl = crl + self._pyfree = _pyfree + else: + self.crl = m2.x509_crl_new() + self._pyfree = 1 + + def __del__(self): + if getattr(self, '_pyfree', 0): + self.m2_x509_crl_free(self.crl) + + def as_text(self): + """ + Return CRL in PEM format in a string. + + @rtype: string + @return: String containing the CRL in PEM format. + """ + buf=BIO.MemoryBuffer() + m2.x509_crl_print(buf.bio_ptr(), self.crl) + return buf.read_all() + + +def load_crl(file): + """ + Load CRL from file. + + @type file: string + @param file: Name of file containing CRL in PEM format. + + @rtype: M2Crypto.X509.CRL + @return: M2Crypto.X509.CRL object. + """ + f=BIO.openfile(file) + cptr=m2.x509_crl_read_pem(f.bio_ptr()) + f.close() + if cptr is None: + raise X509Error(Err.get_error()) + return CRL(cptr, 1) + + diff --git a/M2Crypto/__init__.py b/M2Crypto/__init__.py new file mode 100644 index 0000000..e7acfe7 --- /dev/null +++ b/M2Crypto/__init__.py @@ -0,0 +1,60 @@ +""" +M2Crypto is the most complete Python wrapper for OpenSSL featuring RSA, DSA, +DH, EC, HMACs, message digests, symmetric ciphers (including AES); SSL +functionality to implement clients and servers; HTTPS extensions to +Python's httplib, urllib, and xmlrpclib; unforgeable HMAC'ing AuthCookies +for web session management; FTP/TLS client and server; S/MIME; ZServerSSL: +A HTTPS server for Zope and ZSmime: An S/MIME messenger for Zope. +M2Crypto can also be used to provide SSL for Twisted. Smartcards supported +through the Engine interface. + +Copyright (c) 1999-2004 Ng Pheng Siong. All rights reserved. + +Portions created by Open Source Applications Foundation (OSAF) are +Copyright (C) 2004-2007 OSAF. All Rights Reserved. + +Copyright 2008-2011 Heikki Toivonen. All rights reserved. +""" + +version_info = (0, 21, 1) +version = '.'.join([str(_v) for _v in version_info]) + +import __m2crypto +import m2 +import ASN1 +import AuthCookie +import BIO +import BN +import Rand +import DH +import DSA +if m2.OPENSSL_VERSION_NUMBER >= 0x90800F and m2.OPENSSL_NO_EC == 0: + import EC +import Err +import Engine +import EVP +import RSA +import RC4 +import SMIME +import SSL +import X509 +import PGP +import m2urllib +# Backwards compatibility. +urllib2 = m2urllib + +import sys +if sys.version_info >= (2,4): + import m2urllib2 +del sys + +import ftpslib +import httpslib +import m2xmlrpclib +import threading +import util + +encrypt=1 +decrypt=0 + +__m2crypto.lib_init() diff --git a/M2Crypto/callback.py b/M2Crypto/callback.py new file mode 100644 index 0000000..46613ca --- /dev/null +++ b/M2Crypto/callback.py @@ -0,0 +1,9 @@ +"""Deprecated, use the util module instead. + +Copyright (c) 1999-2003 Ng Pheng Siong. All rights reserved.""" + +import warnings + +warnings.warn('Use the util module instead', DeprecationWarning) + +from util import genparam_callback, passphrase_callback diff --git a/M2Crypto/ftpslib.py b/M2Crypto/ftpslib.py new file mode 100644 index 0000000..b7d82fd --- /dev/null +++ b/M2Crypto/ftpslib.py @@ -0,0 +1,94 @@ +"""M2Crypto client-side FTP/TLS. + +This implementation complies with draft-murray-auth-ftp-ssl-07.txt. + +Example: + +>>> from M2Crypto import ftpslib +>>> f = ftpslib.FTP_TLS() +>>> f.connect('', 9021) +'220 spinnaker.dyndns.org M2Crypto (Medusa) FTP/TLS server v0.07 ready.' +>>> f.auth_tls() +>>> f.set_pasv(0) +>>> f.login('ftp', 'ngps@') +'230 Ok.' +>>> f.retrlines('LIST') +-rw-rw-r-- 1 0 198 2326 Jul 3 1996 apache_pb.gif +drwxrwxr-x 7 0 198 1536 Oct 10 2000 manual +drwxrwxr-x 2 0 198 512 Oct 31 2000 modpy +drwxrwxr-x 2 0 198 512 Oct 31 2000 bobo +drwxr-xr-x 2 0 198 14336 May 28 15:54 postgresql +drwxr-xr-x 4 100 198 512 May 16 17:19 home +drwxr-xr-x 7 100 100 3584 Sep 23 2000 openacs +drwxr-xr-x 10 0 0 512 Aug 5 2000 python1.5 +-rw-r--r-- 1 100 198 326 Jul 29 03:29 index.html +drwxr-xr-x 12 0 0 512 May 31 17:08 python2.1 +'226 Transfer complete' +>>> f.quit() +'221 Goodbye.' +>>> + + +Copyright (c) 1999-2003 Ng Pheng Siong. All rights reserved.""" + +# Python +from ftplib import * +from ftplib import parse150, parse227 +from ftplib import error_reply, error_temp, error_perm, error_proto +import socket, time + +# M2Crypto +import SSL + +DEFAULT_PROTOCOL='sslv23' + +class FTP_TLS(FTP): + + """Python OO interface to client-side FTP/TLS.""" + + def __init__(self, host=None, ssl_ctx=None): + """Initialise the client. If 'host' is supplied, connect to it.""" + if ssl_ctx is not None: + self.ssl_ctx = ssl_ctx + else: + self.ssl_ctx = SSL.Context(DEFAULT_PROTOCOL) + FTP.__init__(self, host) + self.prot = 0 + + def auth_tls(self): + """Secure the control connection per AUTH TLS, aka AUTH TLS-C.""" + self.voidcmd('AUTH TLS') + s = SSL.Connection(self.ssl_ctx, self.sock) + s.setup_ssl() + s.set_connect_state() + s.connect_ssl() + self.sock = s + self.file = self.sock.makefile() + + def auth_ssl(self): + """Secure the control connection per AUTH SSL, aka AUTH TLS-P.""" + raise NotImplementedError + + def prot_p(self): + """Set up secure data connection.""" + self.voidcmd('PBSZ 0') + self.voidcmd('PROT P') + self.prot = 1 + + def prot_c(self): + """Set up data connection in the clear.""" + self.voidcmd('PROT C') + self.prot = 0 + + def ntransfercmd(self, cmd, rest=None): + """Initiate a data transfer.""" + conn, size = FTP.ntransfercmd(self, cmd, rest) + if self.prot: + conn = SSL.Connection(self.ssl_ctx, conn) + conn.setup_ssl() + conn.set_connect_state() + conn.set_session(self.sock.get_session()) + conn.connect_ssl() + return conn, size + + diff --git a/M2Crypto/httpslib.py b/M2Crypto/httpslib.py new file mode 100644 index 0000000..c1bfd78 --- /dev/null +++ b/M2Crypto/httpslib.py @@ -0,0 +1,212 @@ +"""M2Crypto support for Python's httplib. + +Copyright (c) 1999-2004 Ng Pheng Siong. All rights reserved.""" + +import string, sys +import socket +from urlparse import urlsplit, urlunsplit +import base64 + +from httplib import * +from httplib import HTTPS_PORT # This is not imported with just '*' +import SSL + +class HTTPSConnection(HTTPConnection): + + """ + This class allows communication via SSL using M2Crypto. + """ + + default_port = HTTPS_PORT + + def __init__(self, host, port=None, strict=None, **ssl): + self.session = None + keys = ssl.keys() + try: + keys.remove('key_file') + except ValueError: + pass + try: + keys.remove('cert_file') + except ValueError: + pass + try: + keys.remove('ssl_context') + except ValueError: + pass + if keys: + raise ValueError('unknown keyword argument') + try: + self.ssl_ctx = ssl['ssl_context'] + assert isinstance(self.ssl_ctx, SSL.Context), self.ssl_ctx + except KeyError: + self.ssl_ctx = SSL.Context('sslv23') + HTTPConnection.__init__(self, host, port, strict) + + def connect(self): + self.sock = SSL.Connection(self.ssl_ctx) + if self.session: + self.sock.set_session(self.session) + self.sock.connect((self.host, self.port)) + + def close(self): + # This kludges around line 545 of httplib.py, + # which closes the connection in this object; + # the connection remains open in the response + # object. + # + # M2Crypto doesn't close-here-keep-open-there, + # so, in effect, we don't close until the whole + # business is over and gc kicks in. + # + # XXX Long-running callers beware leakage. + # + # XXX 05-Jan-2002: This module works with Python 2.2, + # XXX but I've not investigated if the above conditions + # XXX remain. + pass + + def get_session(self): + return self.sock.get_session() + + def set_session(self, session): + self.session = session + + +class HTTPS(HTTP): + + _connection_class = HTTPSConnection + + def __init__(self, host='', port=None, strict=None, **ssl): + HTTP.__init__(self, host, port, strict) + try: + self.ssl_ctx = ssl['ssl_context'] + except KeyError: + self.ssl_ctx = SSL.Context('sslv23') + assert isinstance(self._conn, HTTPSConnection) + self._conn.ssl_ctx = self.ssl_ctx + + +class ProxyHTTPSConnection(HTTPSConnection): + + """ + An HTTPS Connection that uses a proxy and the CONNECT request. + + When the connection is initiated, CONNECT is first sent to the proxy (along + with authorization headers, if supplied). If successful, an SSL connection + will be established over the socket through the proxy and to the target + host. + + Finally, the actual request is sent over the SSL connection tunneling + through the proxy. + """ + + _ports = {'http' : 80, 'https' : 443} + _AUTH_HEADER = "Proxy-Authorization" + _UA_HEADER = "User-Agent" + + def __init__(self, host, port=None, strict=None, username=None, + password=None, **ssl): + """ + Create the ProxyHTTPSConnection object. + + host and port are the hostname and port number of the proxy server. + """ + HTTPSConnection.__init__(self, host, port, strict, **ssl) + + self._username = username + self._password = password + self._proxy_auth = None + self._proxy_UA = None + + def putrequest(self, method, url, skip_host=0, skip_accept_encoding=0): + #putrequest is called before connect, so can interpret url and get + #real host/port to be used to make CONNECT request to proxy + proto, netloc, path, query, fragment = urlsplit(url) + if not proto: + raise ValueError, "unknown URL type: %s" % url + + #get host & port + try: + username_password, host_port = netloc.split('@') + except ValueError: + host_port = netloc + + try: + host, port = host_port.split(':') + except ValueError: + host = host_port + #try to get port from proto + try: + port = self._ports[proto] + except KeyError: + raise ValueError, "unknown protocol for: %s" % url + + self._real_host = host + self._real_port = int(port) + rest = urlunsplit((None, None, path, query, fragment)) + if sys.version_info < (2,4): + HTTPSConnection.putrequest(self, method, rest, skip_host) + else: + HTTPSConnection.putrequest(self, method, rest, skip_host, skip_accept_encoding) + + def putheader(self, header, value): + # Store the auth header if passed in. + if header.lower() == self._UA_HEADER.lower(): + self._proxy_UA = value + if header.lower() == self._AUTH_HEADER.lower(): + self._proxy_auth = value + else: + HTTPSConnection.putheader(self, header, value) + + def endheaders(self): + # We've recieved all of hte headers. Use the supplied username + # and password for authorization, possibly overriding the authstring + # supplied in the headers. + if not self._proxy_auth: + self._proxy_auth = self._encode_auth() + + HTTPSConnection.endheaders(self) + + def connect(self): + HTTPConnection.connect(self) + + #send proxy CONNECT request + self.sock.sendall(self._get_connect_msg()) + response = HTTPResponse(self.sock) + response.begin() + + code = response.status + if code != 200: + #proxy returned and error, abort connection, and raise exception + self.close() + raise socket.error, "Proxy connection failed: %d" % code + + self._start_ssl() + + def _get_connect_msg(self): + """ Return an HTTP CONNECT request to send to the proxy. """ + msg = "CONNECT %s:%d HTTP/1.1\r\n" % (self._real_host, self._real_port) + msg = msg + "Host: %s:%d\r\n" % (self._real_host, self._real_port) + if self._proxy_UA: + msg = msg + "%s: %s\r\n" % (self._UA_HEADER, self._proxy_UA) + if self._proxy_auth: + msg = msg + "%s: %s\r\n" % (self._AUTH_HEADER, self._proxy_auth) + msg = msg + "\r\n" + return msg + + def _start_ssl(self): + """ Make this connection's socket SSL-aware. """ + self.sock = SSL.Connection(self.ssl_ctx, self.sock) + self.sock.setup_ssl() + self.sock.set_connect_state() + self.sock.connect_ssl() + + def _encode_auth(self): + """ Encode the username and password for use in the auth header. """ + if not (self._username and self._password): + return None + # Authenticated proxy + userpass = "%s:%s" % (self._username, self._password) + enc_userpass = base64.encodestring(userpass).replace("\n", "") + return "Basic %s" % enc_userpass diff --git a/M2Crypto/m2.py b/M2Crypto/m2.py new file mode 100644 index 0000000..e4bb695 --- /dev/null +++ b/M2Crypto/m2.py @@ -0,0 +1,31 @@ +"""M2Crypto low level OpenSSL wrapper functions. + +m2 is the low level wrapper for OpenSSL functions. Typically you would not +need to use these directly, since these will be called by the higher level +objects you should try to use instead. + +Naming conventions: All functions wrapped by m2 are all lower case, +words separated by underscores. + +Examples: + +OpenSSL M2Crypto + +X509_get_version m2.x509_get_version +X509_get_notBefore m2.x509_get_not_before +X509_REQ_verify m2.x509_req_verify + +Exceptions to naming rules: + +XXX TDB + +Copyright (c) 1999-2004 Ng Pheng Siong. All rights reserved. + +Portions created by Open Source Applications Foundation (OSAF) are +Copyright (C) 2004 OSAF. All Rights Reserved. +""" + +from __m2crypto import * +lib_init() + + diff --git a/M2Crypto/m2urllib.py b/M2Crypto/m2urllib.py new file mode 100644 index 0000000..d951eb2 --- /dev/null +++ b/M2Crypto/m2urllib.py @@ -0,0 +1,70 @@ +"""M2Crypto enhancement to Python's urllib for handling +'https' url's. + +Copyright (c) 1999-2003 Ng Pheng Siong. All rights reserved.""" + +import string, sys, urllib +from urllib import * + +import SSL +import httpslib + +DEFAULT_PROTOCOL='sslv23' + +def open_https(self, url, data=None, ssl_context=None): + if ssl_context is not None and isinstance(ssl_context, SSL.Context): + self.ctx = ssl_context + else: + self.ctx = SSL.Context(DEFAULT_PROTOCOL) + user_passwd = None + if type(url) is type(""): + host, selector = splithost(url) + if host: + user_passwd, host = splituser(host) + host = unquote(host) + realhost = host + else: + host, selector = url + urltype, rest = splittype(selector) + url = rest + user_passwd = None + if string.lower(urltype) != 'http': + realhost = None + else: + realhost, rest = splithost(rest) + if realhost: + user_passwd, realhost = splituser(realhost) + if user_passwd: + selector = "%s://%s%s" % (urltype, realhost, rest) + #print "proxy via http:", host, selector + if not host: raise IOError, ('http error', 'no host given') + if user_passwd: + import base64 + auth = string.strip(base64.encodestring(user_passwd)) + else: + auth = None + # Start here! + h = httpslib.HTTPSConnection(host=host, ssl_context=self.ctx) + #h.set_debuglevel(1) + # Stop here! + if data is not None: + h.putrequest('POST', selector) + h.putheader('Content-type', 'application/x-www-form-urlencoded') + h.putheader('Content-length', '%d' % len(data)) + else: + h.putrequest('GET', selector) + if auth: h.putheader('Authorization', 'Basic %s' % auth) + for args in self.addheaders: apply(h.putheader, args) + h.endheaders() + if data is not None: + h.send(data + '\r\n') + # Here again! + resp = h.getresponse() + fp = resp.fp + return urllib.addinfourl(fp, resp.msg, "https:" + url) + # Stop again. + +# Minor brain surgery. +URLopener.open_https = open_https + + diff --git a/M2Crypto/m2urllib2.py b/M2Crypto/m2urllib2.py new file mode 100644 index 0000000..e500410 --- /dev/null +++ b/M2Crypto/m2urllib2.py @@ -0,0 +1,152 @@ +""" +M2Crypto enhancement to Python's urllib2 for handling +'https' url's. + +Code from urllib2 is Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007 +Python Software Foundation; All Rights Reserved + +Summary of changes: + - Use an HTTPSProxyConnection if the request is going through a proxy. + - Add the SSL context to the https connection when performing https_open. + - Add the M2Crypto HTTPSHandler when building a default opener. +""" + +import socket +from urllib2 import * +import urlparse + +import SSL +import httpslib + + +class _closing_fileobject(socket._fileobject): + '''socket._fileobject that propagates self.close() to the socket. + + Python 2.5 provides this as socket._fileobject(sock, close=True). + ''' + + def __init__(self, sock): + socket._fileobject.__init__(self, sock) + + def close(self): + sock = self._sock + socket._fileobject.close(self) + sock.close() + +class HTTPSHandler(AbstractHTTPHandler): + def __init__(self, ssl_context = None): + AbstractHTTPHandler.__init__(self) + + if ssl_context is not None: + assert isinstance(ssl_context, SSL.Context), ssl_context + self.ctx = ssl_context + else: + self.ctx = SSL.Context() + + # Copied from urllib2, so we can set the ssl context. + def https_open(self, req): + """Return an addinfourl object for the request, using http_class. + + http_class must implement the HTTPConnection API from httplib. + The addinfourl return value is a file-like object. It also + has methods and attributes including: + - info(): return a mimetools.Message object for the headers + - geturl(): return the original request URL + - code: HTTP status code + """ + host = req.get_host() + if not host: + raise URLError('no host given') + + # Our change: Check to see if we're using a proxy. + # Then create an appropriate ssl-aware connection. + full_url = req.get_full_url() + target_host = urlparse.urlparse(full_url)[1] + + if (target_host != host): + h = httpslib.ProxyHTTPSConnection(host = host, ssl_context = self.ctx) + else: + h = httpslib.HTTPSConnection(host = host, ssl_context = self.ctx) + # End our change + h.set_debuglevel(self._debuglevel) + + headers = dict(req.headers) + headers.update(req.unredirected_hdrs) + # We want to make an HTTP/1.1 request, but the addinfourl + # class isn't prepared to deal with a persistent connection. + # It will try to read all remaining data from the socket, + # which will block while the server waits for the next request. + # So make sure the connection gets closed after the (only) + # request. + headers["Connection"] = "close" + try: + h.request(req.get_method(), req.get_selector(), req.data, headers) + r = h.getresponse() + except socket.error, err: # XXX what error? + raise URLError(err) + + # Pick apart the HTTPResponse object to get the addinfourl + # object initialized properly. + + # Wrap the HTTPResponse object in socket's file object adapter + # for Windows. That adapter calls recv(), so delegate recv() + # to read(). This weird wrapping allows the returned object to + # have readline() and readlines() methods. + + # XXX It might be better to extract the read buffering code + # out of socket._fileobject() and into a base class. + + r.recv = r.read + fp = _closing_fileobject(r) + + resp = addinfourl(fp, r.msg, req.get_full_url()) + resp.code = r.status + resp.msg = r.reason + return resp + + + https_request = AbstractHTTPHandler.do_request_ + + +# Copied from urllib2 with modifications for ssl +def build_opener(ssl_context = None, *handlers): + """Create an opener object from a list of handlers. + + The opener will use several default handlers, including support + for HTTP and FTP. + + If any of the handlers passed as arguments are subclasses of the + default handlers, the default handlers will not be used. + """ + import types + def isclass(obj): + return isinstance(obj, types.ClassType) or hasattr(obj, "__bases__") + + opener = OpenerDirector() + default_classes = [ProxyHandler, UnknownHandler, HTTPHandler, + HTTPDefaultErrorHandler, HTTPRedirectHandler, + FTPHandler, FileHandler, HTTPErrorProcessor] + skip = [] + for klass in default_classes: + for check in handlers: + if isclass(check): + if issubclass(check, klass): + skip.append(klass) + elif isinstance(check, klass): + skip.append(klass) + for klass in skip: + default_classes.remove(klass) + + for klass in default_classes: + opener.add_handler(klass()) + + # Add the HTTPS handler with ssl_context + if HTTPSHandler not in skip: + opener.add_handler(HTTPSHandler(ssl_context)) + + + for h in handlers: + if isclass(h): + h = h() + opener.add_handler(h) + return opener diff --git a/M2Crypto/m2xmlrpclib.py b/M2Crypto/m2xmlrpclib.py new file mode 100644 index 0000000..bb50a01 --- /dev/null +++ b/M2Crypto/m2xmlrpclib.py @@ -0,0 +1,65 @@ +"""M2Crypto enhancement to xmlrpclib. + +Copyright (c) 1999-2003 Ng Pheng Siong. All rights reserved.""" + +import base64, string, sys + +from xmlrpclib import * +import M2Crypto +import SSL, httpslib, m2urllib + +__version__ = M2Crypto.version + +class SSL_Transport(Transport): + + user_agent = "M2Crypto_XMLRPC/%s - %s" % (__version__, Transport.user_agent) + + def __init__(self, ssl_context=None, *args, **kw): + if getattr(Transport, '__init__', None) is not None: + Transport.__init__(self, *args, **kw) + if ssl_context is None: + self.ssl_ctx=SSL.Context('sslv23') + else: + self.ssl_ctx=ssl_context + + def request(self, host, handler, request_body, verbose=0): + # Handle username and password. + user_passwd, host_port = m2urllib.splituser(host) + _host, _port = m2urllib.splitport(host_port) + h = httpslib.HTTPS(_host, int(_port), ssl_context=self.ssl_ctx) + if verbose: + h.set_debuglevel(1) + + # What follows is as in xmlrpclib.Transport. (Except the authz bit.) + h.putrequest("POST", handler) + + # required by HTTP/1.1 + h.putheader("Host", _host) + + # required by XML-RPC + h.putheader("User-Agent", self.user_agent) + h.putheader("Content-Type", "text/xml") + h.putheader("Content-Length", str(len(request_body))) + + # Authorisation. + if user_passwd is not None: + auth=string.strip(base64.encodestring(user_passwd)) + h.putheader('Authorization', 'Basic %s' % auth) + + h.endheaders() + + if request_body: + h.send(request_body) + + errcode, errmsg, headers = h.getreply() + + if errcode != 200: + raise ProtocolError( + host + handler, + errcode, errmsg, + headers + ) + + self.verbose = verbose + return self.parse_response(h.getfile()) + diff --git a/M2Crypto/threading.py b/M2Crypto/threading.py new file mode 100644 index 0000000..4cb8149 --- /dev/null +++ b/M2Crypto/threading.py @@ -0,0 +1,20 @@ +""" +M2Crypto threading support, required for multithreaded applications. + +Copyright (c) 1999-2003 Ng Pheng Siong. All rights reserved.""" + +# M2Crypto +import m2 + +def init(): + """ + Initialize threading support. + """ + m2.threading_init() + +def cleanup(): + """ + End and cleanup threading support. + """ + m2.threading_cleanup() + diff --git a/M2Crypto/util.py b/M2Crypto/util.py new file mode 100644 index 0000000..12103fc --- /dev/null +++ b/M2Crypto/util.py @@ -0,0 +1,71 @@ +""" + M2Crypto utility routines. + + Copyright (c) 1999-2004 Ng Pheng Siong. All rights reserved. + + Portions created by Open Source Applications Foundation (OSAF) are + Copyright (C) 2004 OSAF. All Rights Reserved. +""" + +import sys +import m2 + +class UtilError(Exception): pass + +m2.util_init(UtilError) + +def h2b(s): + import array, string + ar=array.array('c') + start=0 + if s[:2]=='0x': + start=2 + for i in range(start, len(s), 2): + num=string.atoi("%s"%(s[i:i+2],), 16) + ar.append(chr(num)) + return ar.tostring() + +def pkcs5_pad(data, blklen=8): + pad=(8-(len(data)%8)) + return data+chr(pad)*pad + +def pkcs7_pad(data, blklen): + if blklen>255: + raise ValueError, 'illegal block size' + pad=(blklen-(len(data)%blklen)) + return data+chr(pad)*pad + +def octx_to_num(x): + v = 0L + lx = len(x) + for i in range(lx): + v = v + ord(x[i]) * (256L ** (lx-i-1)) + return v + +def genparam_callback(p, n, out=sys.stdout): + ch = ['.','+','*','\n'] + out.write(ch[p]) + out.flush() + +def quiet_genparam_callback(p, n, out): + pass + +def passphrase_callback(v, prompt1='Enter passphrase:', + prompt2='Verify passphrase:'): + from getpass import getpass + while 1: + try: + p1=getpass(prompt1) + if v: + p2=getpass(prompt2) + if p1==p2: + break + else: + break + except KeyboardInterrupt: + return None + return p1 + +def no_passphrase_callback(*args): + return '' + diff --git a/PKG-INFO b/PKG-INFO new file mode 100644 index 0000000..3972df5 --- /dev/null +++ b/PKG-INFO @@ -0,0 +1,23 @@ +Metadata-Version: 1.0 +Name: M2Crypto +Version: 0.21.1 +Summary: M2Crypto: A Python crypto and SSL toolkit +Home-page: http://chandlerproject.org/Projects/MeTooCrypto +Author: Heikki Toivonen +Author-email: heikki@osafoundation.org +License: BSD-style license +Description: M2Crypto is the most complete Python wrapper for OpenSSL featuring RSA, DSA, + DH, EC, HMACs, message digests, symmetric ciphers (including AES); SSL + functionality to implement clients and servers; HTTPS extensions to Python's + httplib, urllib, and xmlrpclib; unforgeable HMAC'ing AuthCookies for web + session management; FTP/TLS client and server; S/MIME; ZServerSSL: A HTTPS + server for Zope and ZSmime: An S/MIME messenger for Zope. M2Crypto can also be + used to provide SSL for Twisted. +Platform: any +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: C +Classifier: Programming Language :: Python +Classifier: Topic :: Security :: Cryptography +Classifier: Topic :: Software Development :: Libraries :: Python Modules @@ -0,0 +1,61 @@ +========= + M2Crypto +========= + +:Maintainer: Heikki Toivonen +:Web-Site: http://chandlerproject.org/Projects/MeTooCrypto + + +M2Crypto = Python + OpenSSL + SWIG +------------------------------------ + +M2Crypto is a crypto and SSL toolkit for Python. + +M2 stands for "me, too!" + +M2Crypto comes with the following: + +- **RSA**, **DSA**, **DH**, **HMACs**, **message digests**, + **symmetric ciphers** including **AES**, + +- **SSL** functionality to implement **clients and servers**. + +- **Example SSL client and server programs**, which are variously + **threading**, **forking** or based on **non-blocking socket IO**. + +- **HTTPS** extensions to Python's **httplib, urllib and xmlrpclib**. + +- Unforgeable HMAC'ing **AuthCookies** for **web session management**. + +- **FTP/TLS** client and server. + +- **S/MIME v2**. + +- **ZServerSSL**: A **HTTPS server for Zope**. + +- **ZSmime**: An S/MIME messenger for **Zope**. + +- And much more. + +M2Crypto is released under a very liberal BSD-style licence. See +LICENCE for details. + +To install, see the file INSTALL. + +Look at the tests and demos for example use. Recommended reading before +deploying in production is "Network Security with OpenSSL" by John Viega, +Matt Messier and Pravir Chandra, ISBN 059600270X. + +Note these caveats: + +- Possible memory leaks, because some objects need to be freed on the + Python side and other objects on the C side, and these may change + between OpenSSL versions. (Multiple free's lead to crashes very + quickly, so these should be relatively rare.) + +- No memory locking/clearing for keys, passphrases, etc. because AFAIK + Python does not provide the features needed. On the C (OpenSSL) side + things are cleared when the Python objects are deleted. + + +Have fun! Your feedback is welcome. diff --git a/SWIG/Makefile b/SWIG/Makefile new file mode 100644 index 0000000..7d73a3b --- /dev/null +++ b/SWIG/Makefile @@ -0,0 +1,25 @@ +# $Id: Makefile 299 2005-06-09 17:32:28Z heikki $ + +CFLAGS = -DTHREADING -g +INCLUDE = -I/usr/local/include -I. +LIBS = -L/usr/local/lib -lssl -lcrypto +#if PYTHON_VERSION +PYVER = $(PYTHON_VERSION) +#else +PYVER = 2.3 +#endif +PYINCLUDE = -DHAVE_CONFIG_H -I/usr/local/include/python$(PYVER) \ + -I/usr/local/lib/python$(PYVER)/config +PYLIB = /usr/local/lib/python$(PYVER)/config + +all: _m2crypto + +_m2crypto: _m2crypto.i + swig -python -shadow _m2crypto.i + cc -c -fpic $(CFLAGS) $(INCLUDE) $(PYINCLUDE) _m2crypto_wrap.c + ld -Bshareable -o __m2crypto.so _m2crypto_wrap.o $(LIBS) + cp _m2crypto.py __m2crypto.so ../M2Crypto + +clean: + rm -f *_wrap* *.o *.so _*.py *.pyc + diff --git a/SWIG/Makefile.mw b/SWIG/Makefile.mw new file mode 100644 index 0000000..02f5ae5 --- /dev/null +++ b/SWIG/Makefile.mw @@ -0,0 +1,34 @@ +# $Id: Makefile.mw 299 2005-06-09 17:32:28Z heikki $ + +# Python +PYFLAGS=-D__WIN32__ -DHAVE_CONFIG_H -Ic:/pkg/py23/include +PYLIB=c:/pkg/py23/libs/libpython23.a +PYINCLUDE=-Ic:/pkg/py23/include + +# OpenSSL +SSLINCLUDE=-Ic:/pkg/openssl/include +SSLLIB=c:/pkg/openssl/lib/libssl32.a c:/pkg/openssl/lib/libeay32.a + +# Windoze +INCLUDE=$(PYINCLUDE) $(SSLINCLUDE) -I. +LIBS=$(PYLIB) $(SSLLIB) + +SWIG=c:/pkg/swig/swig.exe +SWIGFLAGS=-shadow -python #-verbose + +CP=cp + +all: swig + +swig: _m2crypto.i + $(SWIG) $(SWIGFLAGS) _m2crypto.i + gcc -c -DTHREADING -g $(INCLUDE) _m2crypto_wrap.c + dllwrap --dllname __m2crypto.pyd --driver-name gcc \ + --def _m2crypto.def -o __m2crypto.pyd _m2crypto_wrap.o \ + -s --entry _DllMain@12 --target=i386-mingw32 $(LIBS) + $(CP) _m2crypto.py ..\M2Crypto + $(CP) __m2crypto.pyd ..\M2Crypto + +clean: + del *wrap* *.o *.dll *.exp *.ilk *.pdb *.lib _*.py *.pyc + diff --git a/SWIG/Makefile.osx b/SWIG/Makefile.osx new file mode 100644 index 0000000..a8508a6 --- /dev/null +++ b/SWIG/Makefile.osx @@ -0,0 +1,32 @@ +# $Id: Makefile.osx 299 2005-06-09 17:32:28Z heikki $ + +# 2003-10-26, ngps: Beware any mixup of tabs and spaces caused by me. + +WHICHOPENSSL = /usr/local + +CFLAGS = -DTHREADING -DHAVE_CONFIG -g -O2 +INCLUDE = -I. -I/Library/Frameworks/Python.framework/Headers +LIBS = $(WHICHOPENSSL)/lib/libssl.a \ + $(WHICHOPENSSL)/lib/libcrypto.a \ + /Library/Frameworks/Python.framework/python + +all: __m2crypto.so + +_m2crypto_wrap.c: _m2crypto.i + swig -shadow -python _m2crypto.i + +_m2crypto_wrap.o: _m2crypto_wrap.c + cc -c $(CFLAGS) $(INCLUDES) _m2crypto_wrap.c + +__m2crypto.so: _m2crypto_wrap.o + cc -bundle _m2crypto_wrap.o $(LIBS) -lcc_dynamic -o __m2crypto.so + cp __m2crypto.so ../M2Crypto + +clean: + rm -f *_wrap* *.o *.so _*.py *.pyc + +versions: + python -c "import sys, os; \ + print os.popen('gcc --version').readlines()[0],; \ + print 'Python '+sys.version.split()[0]; \ + print os.popen('$(WHICHOPENSSL)/bin/openssl version').readlines()[0]" diff --git a/SWIG/_aes.i b/SWIG/_aes.i new file mode 100644 index 0000000..be0af87 --- /dev/null +++ b/SWIG/_aes.i @@ -0,0 +1,85 @@ +/* Copyright (c) 1999-2004 Ng Pheng Siong. All rights reserved. */ +/* $Id: _aes.i 721 2010-02-13 06:30:33Z heikki $ */ + +%{ +#include <openssl/evp.h> + +#if OPENSSL_VERSION_NUMBER >= 0x0090800fL +#include <openssl/aes.h> +#endif + +/* +// 2004-10-10, ngps: +// CTR mode is not included in the default OpenSSL build. +// To use the AES CTR ciphers, link with your own copy of OpenSSL. +*/ +#ifdef HAVE_AES_CTR +extern EVP_CIPHER const *EVP_aes_128_ctr(void); +extern EVP_CIPHER const *EVP_aes_192_ctr(void); +extern EVP_CIPHER const *EVP_aes_256_ctr(void); +#endif +%} + +%apply Pointer NONNULL { AES_KEY * }; + +%constant int AES_BLOCK_SIZE = AES_BLOCK_SIZE; + +%inline %{ +AES_KEY *aes_new(void) { + AES_KEY *key; + + if (!(key = (AES_KEY *)PyMem_Malloc(sizeof(AES_KEY)))) + PyErr_SetString(PyExc_MemoryError, "aes_new"); + return key; +} + +void AES_free(AES_KEY *key) { + PyMem_Free((void *)key); +} + +/* +// op == 0: decrypt +// otherwise: encrypt (Python code will supply the value 1.) +*/ +PyObject *AES_set_key(AES_KEY *key, PyObject *value, int bits, int op) { + const void *vbuf; + Py_ssize_t vlen; + + if (PyObject_AsReadBuffer(value, &vbuf, &vlen) == -1) + return NULL; + + if (op == 0) + AES_set_encrypt_key(vbuf, bits, key); + else + AES_set_decrypt_key(vbuf, bits, key); + Py_INCREF(Py_None); + return Py_None; +} + +/* +// op == 0: decrypt +// otherwise: encrypt (Python code will supply the value 1.) +*/ +PyObject *AES_crypt(const AES_KEY *key, PyObject *in, int outlen, int op) { + const void *buf; + Py_ssize_t len; + unsigned char *out; + + if (PyObject_AsReadBuffer(in, &buf, &len) == -1) + return NULL; + + if (!(out=(unsigned char *)PyMem_Malloc(outlen))) { + PyErr_SetString(PyExc_MemoryError, "AES_crypt"); + return NULL; + } + if (op == 0) + AES_encrypt((const unsigned char *)in, out, key); + else + AES_decrypt((const unsigned char *)in, out, key); + return PyString_FromStringAndSize((char*)out, outlen); +} + +int AES_type_check(AES_KEY *key) { + return 1; +} +%} diff --git a/SWIG/_asn1.i b/SWIG/_asn1.i new file mode 100644 index 0000000..6dab7ff --- /dev/null +++ b/SWIG/_asn1.i @@ -0,0 +1,201 @@ +/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* Copyright (c) 1999-2004 Ng Pheng Siong. All rights reserved. */ +/* +** Portions created by Open Source Applications Foundation (OSAF) are +** Copyright (C) 2004 OSAF. All Rights Reserved. +*/ +/* $Id: _asn1.i 696 2009-07-28 03:43:19Z heikki $ */ + +%{ +#include <openssl/asn1.h> +%} + +%apply Pointer NONNULL { BIO * }; +%apply Pointer NONNULL { ASN1_OBJECT * }; +%apply Pointer NONNULL { ASN1_STRING * }; +%apply Pointer NONNULL { ASN1_INTEGER * }; +%apply Pointer NONNULL { ASN1_UTCTIME * }; + +%rename(asn1_object_new) ASN1_OBJECT_new; +extern ASN1_OBJECT *ASN1_OBJECT_new( void ); +%rename(asn1_object_create) ASN1_OBJECT_create; +extern ASN1_OBJECT *ASN1_OBJECT_create( int, unsigned char *, int, const char *, const char *); +%rename(asn1_object_free) ASN1_OBJECT_free; +extern void ASN1_OBJECT_free( ASN1_OBJECT *); +%rename(i2d_asn1_object) i2d_ASN1_OBJECT; +extern int i2d_ASN1_OBJECT( ASN1_OBJECT *, unsigned char **); +%rename(c2i_asn1_object) c2i_ASN1_OBJECT; +extern ASN1_OBJECT *c2i_ASN1_OBJECT( ASN1_OBJECT **, CONST098 unsigned char **, long); +%rename(d2i_asn1_object) d2i_ASN1_OBJECT; +extern ASN1_OBJECT *d2i_ASN1_OBJECT( ASN1_OBJECT **, CONST098 unsigned char **, long); + +%rename(asn1_bit_string_new) ASN1_BIT_STRING_new; +extern ASN1_BIT_STRING *ASN1_BIT_STRING_new( void ); + +%rename(asn1_string_new) ASN1_STRING_new; +extern ASN1_STRING *ASN1_STRING_new( void ); +%rename(asn1_string_free) ASN1_STRING_free; +extern void ASN1_STRING_free( ASN1_STRING *); + +%typemap(in) (const void *, int) { + if (PyString_Check($input)) { + Py_ssize_t len; + + $1 = PyString_AsString($input); + len = PyString_Size($input); + if (len > INT_MAX) { + PyErr_SetString(PyExc_ValueError, "object too large"); + return NULL; + } + $2 = len; + } else { + PyErr_SetString(PyExc_TypeError, "expected string"); + return NULL; + } +} + +%rename(asn1_string_set) ASN1_STRING_set; +extern int ASN1_STRING_set( ASN1_STRING *, const void *, int); + +%typemap(in) (const void *, int); + +%rename(asn1_string_print) ASN1_STRING_print; +%threadallow ASN1_STRING_print; +extern int ASN1_STRING_print(BIO *, ASN1_STRING *); +%threadallow ASN1_STRING_print_ex; +%rename(asn1_string_print_ex) ASN1_STRING_print_ex; +extern int ASN1_STRING_print_ex(BIO *, ASN1_STRING *, unsigned long); + +%rename(asn1_utctime_new) ASN1_UTCTIME_new; +extern ASN1_UTCTIME *ASN1_UTCTIME_new( void ); +%rename(asn1_utctime_free) ASN1_UTCTIME_free; +extern void ASN1_UTCTIME_free(ASN1_UTCTIME *); +%rename(asn1_utctime_check) ASN1_UTCTIME_check; +extern int ASN1_UTCTIME_check(ASN1_UTCTIME *); +%rename(asn1_utctime_set) ASN1_UTCTIME_set; +extern ASN1_UTCTIME *ASN1_UTCTIME_set(ASN1_UTCTIME *, long); +%rename(asn1_utctime_set_string) ASN1_UTCTIME_set_string; +extern int ASN1_UTCTIME_set_string(ASN1_UTCTIME *, CONST098 char *); +%rename(asn1_utctime_print) ASN1_UTCTIME_print; +%threadallow ASN1_UTCTIME_print; +extern int ASN1_UTCTIME_print(BIO *, ASN1_UTCTIME *); + +%rename(asn1_integer_new) ASN1_INTEGER_new; +extern ASN1_INTEGER *ASN1_INTEGER_new( void ); +%rename(asn1_integer_free) ASN1_INTEGER_free; +extern void ASN1_INTEGER_free( ASN1_INTEGER *); +%rename(asn1_integer_cmp) ASN1_INTEGER_cmp; +extern int ASN1_INTEGER_cmp(ASN1_INTEGER *, ASN1_INTEGER *); + +%constant int ASN1_STRFLGS_ESC_2253 = 1; +%constant int ASN1_STRFLGS_ESC_CTRL = 2; +%constant int ASN1_STRFLGS_ESC_MSB = 4; +%constant int ASN1_STRFLGS_ESC_QUOTE = 8; +%constant int ASN1_STRFLGS_UTF8_CONVERT = 0x10; +%constant int ASN1_STRFLGS_DUMP_UNKNOWN = 0x100; +%constant int ASN1_STRFLGS_DUMP_DER = 0x200; +%constant int ASN1_STRFLGS_RFC2253 = (ASN1_STRFLGS_ESC_2253 | \ + ASN1_STRFLGS_ESC_CTRL | \ + ASN1_STRFLGS_ESC_MSB | \ + ASN1_STRFLGS_UTF8_CONVERT | \ + ASN1_STRFLGS_DUMP_UNKNOWN | \ + ASN1_STRFLGS_DUMP_DER); + +%inline %{ +/* ASN1_UTCTIME_set_string () is a macro */ +int asn1_utctime_type_check(ASN1_UTCTIME *ASN1_UTCTIME) { + return 1; +} + +PyObject *asn1_integer_get(ASN1_INTEGER *asn1) { + BIGNUM *bn; + PyObject *ret; + char *hex; + + bn = ASN1_INTEGER_to_BN(asn1, NULL); + + if (!bn){ + PyErr_SetString( + PyExc_RuntimeError, ERR_reason_error_string(ERR_get_error())); + return NULL; + } + + hex = BN_bn2hex(bn); + + if (!hex){ + PyErr_SetString( + PyExc_RuntimeError, ERR_reason_error_string(ERR_get_error())); + BN_free(bn); + return NULL; + } + + BN_free(bn); + + ret = PyLong_FromString(hex, NULL, 16); + + OPENSSL_free(hex); + + return ret; +} + +int asn1_integer_set(ASN1_INTEGER *asn1, PyObject *value) { + BIGNUM *bn = NULL; + PyObject *fmt, *args, *hex; + + if (PyInt_Check(value)) + return ASN1_INTEGER_set(asn1, PyInt_AS_LONG(value)); + + if (!PyLong_Check(value)){ + PyErr_SetString(PyExc_TypeError, "expected int or long"); + return 0; + } + + fmt = PyString_FromString("%x"); + + if (!fmt) + return 0; + + args = PyTuple_New(1); + + if (!args){ + Py_DECREF(fmt); + PyErr_SetString(PyExc_RuntimeError, "PyTuple_New() failed"); + return 0; + } + + Py_INCREF(value); + PyTuple_SET_ITEM(args, 0, value); + hex = PyString_Format(fmt, args); + + if (!hex){ + PyErr_SetString(PyExc_RuntimeError, "PyString_Format() failed"); + Py_DECREF(fmt); + Py_DECREF(args); + return 0; + } + + Py_DECREF(fmt); + Py_DECREF(args); + + if (BN_hex2bn(&bn, PyString_AsString(hex)) <= 0){ + PyErr_SetString( + PyExc_RuntimeError, ERR_reason_error_string(ERR_get_error())); + Py_DECREF(hex); + return 0; + } + + Py_DECREF(hex); + + if (!BN_to_ASN1_INTEGER(bn, asn1)){ + PyErr_SetString( + PyExc_RuntimeError, ERR_reason_error_string(ERR_get_error())); + BN_free(bn); + return 0; + } + + BN_free(bn); + + return 1; +} + +%} diff --git a/SWIG/_bio.i b/SWIG/_bio.i new file mode 100644 index 0000000..441ba06 --- /dev/null +++ b/SWIG/_bio.i @@ -0,0 +1,227 @@ +/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* Copyright (c) 1999 Ng Pheng Siong. All rights reserved. + * + * Portions created by Open Source Applications Foundation (OSAF) are + * Copyright (C) 2004-2005 OSAF. All Rights Reserved. + * Author: Heikki Toivonen +*/ +/* $Id: _bio.i 695 2009-07-24 06:37:01Z heikki $ */ + +%{ +#include <openssl/bio.h> +%} + +%apply Pointer NONNULL { BIO * }; +%apply Pointer NONNULL { BIO_METHOD * }; + +%rename(bio_s_bio) BIO_s_bio; +extern BIO_METHOD *BIO_s_bio(void); +%rename(bio_s_mem) BIO_s_mem; +extern BIO_METHOD *BIO_s_mem(void); +%rename(bio_s_socket) BIO_s_socket; +extern BIO_METHOD *BIO_s_socket(void); +%rename(bio_f_ssl) BIO_f_ssl; +extern BIO_METHOD *BIO_f_ssl(void); +%rename(bio_f_buffer) BIO_f_buffer; +extern BIO_METHOD *BIO_f_buffer(void); +%rename(bio_f_cipher) BIO_f_cipher; +extern BIO_METHOD *BIO_f_cipher(void); + +%rename(bio_new) BIO_new; +extern BIO *BIO_new(BIO_METHOD *); +%rename(bio_new_socket) BIO_new_socket; +extern BIO *BIO_new_socket(int, int); +%rename(bio_new_fd) BIO_new_fd; +extern BIO *BIO_new_fd(int, int); +%rename(bio_new_fp) BIO_new_fp; +extern BIO *BIO_new_fp(FILE *, int); +%rename(bio_new_file) BIO_new_file; +extern BIO *BIO_new_file(const char *, const char *); +%rename(bio_free) BIO_free; +%threadallow BIO_free; +extern int BIO_free(BIO *); +%rename(bio_free_all) BIO_free_all; +%threadallow BIO_free_all; +extern void BIO_free_all(BIO *); +%rename(bio_dup_chain) BIO_dup_chain; +extern BIO *BIO_dup_chain(BIO *); + +%rename(bio_push) BIO_push; +extern BIO *BIO_push(BIO *, BIO *); +%rename(bio_pop) BIO_pop; +extern BIO *BIO_pop(BIO *); + +%constant int bio_noclose = BIO_NOCLOSE; +%constant int bio_close = BIO_CLOSE; +%constant int BIO_FLAGS_READ = 0x01; +%constant int BIO_FLAGS_WRITE = 0x02; +%constant int BIO_FLAGS_IO_SPECIAL = 0x04; +%constant int BIO_FLAGS_RWS = (BIO_FLAGS_READ|BIO_FLAGS_WRITE|BIO_FLAGS_IO_SPECIAL); +%constant int BIO_FLAGS_SHOULD_RETRY = 0x08; +%constant int BIO_FLAGS_MEM_RDONLY = 0x200; + +%inline %{ +static PyObject *_bio_err; + +void bio_init(PyObject *bio_err) { + Py_INCREF(bio_err); + _bio_err = bio_err; +} + +PyObject *bio_read(BIO *bio, int num) { + PyObject *blob; + void *buf; + int r; + + if (!(buf = PyMem_Malloc(num))) { + PyErr_SetString(PyExc_MemoryError, "bio_read"); + return NULL; + } + Py_BEGIN_ALLOW_THREADS + r = BIO_read(bio, buf, num); + Py_END_ALLOW_THREADS + if (r < 0) { + PyMem_Free(buf); + if (ERR_peek_error()) { + PyErr_SetString(_bio_err, ERR_reason_error_string(ERR_get_error())); + return NULL; + } + Py_INCREF(Py_None); + return Py_None; + } + blob = PyString_FromStringAndSize(buf, r); + PyMem_Free(buf); + return blob; +} + +PyObject *bio_gets(BIO *bio, int num) { + PyObject *blob; + void *buf; + int r; + + if (!(buf = PyMem_Malloc(num))) { + PyErr_SetString(PyExc_MemoryError, "bio_gets"); + return NULL; + } + Py_BEGIN_ALLOW_THREADS + r = BIO_gets(bio, buf, num); + Py_END_ALLOW_THREADS + if (r < 0) { + PyMem_Free(buf); + if (ERR_peek_error()) { + PyErr_SetString(_bio_err, ERR_reason_error_string(ERR_get_error())); + return NULL; + } + Py_INCREF(Py_None); + return Py_None; + } + blob = PyString_FromStringAndSize(buf, r); + PyMem_Free(buf); + return blob; +} + +int bio_write(BIO *bio, PyObject *from) { + const void *fbuf; + int flen, ret; + + if (m2_PyObject_AsReadBufferInt(from, &fbuf, &flen) == -1) + return -1; + + Py_BEGIN_ALLOW_THREADS + ret = BIO_write(bio, fbuf, flen); + Py_END_ALLOW_THREADS + if (ret < 0) { + if (ERR_peek_error()) { + PyErr_SetString(_bio_err, ERR_reason_error_string(ERR_get_error())); + } + } + return ret; +} + +/* XXX Casting size_t to int. */ +int bio_ctrl_pending(BIO *bio) { + return (int)BIO_ctrl_pending(bio); +} + +int bio_ctrl_wpending(BIO *bio) { + return (int)BIO_ctrl_wpending(bio); +} + +int bio_ctrl_get_write_guarantee(BIO *a) { + return BIO_ctrl_get_write_guarantee(a); +} + +int bio_reset(BIO *bio) { + return (int)BIO_reset(bio); +} +%} + +%threadallow bio_flush; +%inline %{ +int bio_flush(BIO *bio) { + return (int)BIO_flush(bio); +} + +int bio_seek(BIO *bio, int offset) { + return (int)BIO_seek(bio, offset); +} + +void bio_set_flags(BIO *bio, int flags) { + BIO_set_flags(bio, flags); +} + +int bio_get_flags(BIO *bio) { + return BIO_get_flags(bio); +} + +PyObject *bio_set_cipher(BIO *b, EVP_CIPHER *c, PyObject *key, PyObject *iv, int op) { + const void *kbuf, *ibuf; + Py_ssize_t klen, ilen; + + if ((PyObject_AsReadBuffer(key, &kbuf, &klen) == -1) + || (PyObject_AsReadBuffer(iv, &ibuf, &ilen) == -1)) + return NULL; + + BIO_set_cipher(b, (const EVP_CIPHER *)c, + (unsigned char *)kbuf, (unsigned char *)ibuf, op); + Py_INCREF(Py_None); + return Py_None; +} + +int bio_set_mem_eof_return(BIO *b, int v) { + return (int)BIO_set_mem_eof_return(b, v); +} + +int bio_get_fd(BIO *bio) { + return BIO_get_fd(bio, NULL); +} +%} + +%threadallow bio_do_handshake; +%inline %{ +int bio_do_handshake(BIO *bio) { + return BIO_do_handshake(bio); +} + +/* macro */ +int bio_make_bio_pair(BIO* b1, BIO* b2) { + return BIO_make_bio_pair(b1, b2); +} + +int bio_set_write_buf_size(BIO* b, size_t size) { + return BIO_set_write_buf_size(b, size); +} + +int bio_should_retry(BIO* a) { + return BIO_should_retry(a); +} + +int bio_should_read(BIO* a) { + return BIO_should_read(a); +} + +int bio_should_write(BIO* a) { + return BIO_should_write(a); +} +%} + diff --git a/SWIG/_bn.i b/SWIG/_bn.i new file mode 100755 index 0000000..a3b73c4 --- /dev/null +++ b/SWIG/_bn.i @@ -0,0 +1,114 @@ +/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* Copyright (c) 2005-2006 Open Source Applications Foundation. All rights reserved. */ + +/* We are converting between the Python arbitrarily long integer and + * the BIGNUM arbitrarily long integer by converting to and from + * a string representation of the number (in hexadecimal). + * Direct manipulation would be a possibility, but would require + * tighter integration with the Python and OpenSSL internals. + */ + + +%{ +#include <openssl/bn.h> +%} + + +%inline %{ +PyObject *bn_rand(int bits, int top, int bottom) +{ + BIGNUM rnd; + PyObject *ret; + char *randhex; + + BN_init(&rnd); + if (!BN_rand(&rnd, bits, top, bottom)) { + /*Custom errors?*/ + PyErr_SetString(PyExc_Exception, ERR_reason_error_string(ERR_get_error())); + BN_free(&rnd); + return NULL; + } + + randhex = BN_bn2hex(&rnd); + if (!randhex) { + /*Custom errors?*/ + PyErr_SetString(PyExc_Exception, ERR_reason_error_string(ERR_get_error())); + BN_free(&rnd); + return NULL; + } + BN_free(&rnd); + + ret = PyLong_FromString(randhex, NULL, 16); + OPENSSL_free(randhex); + return ret; +} + + +PyObject *bn_rand_range(PyObject *range) +{ + BIGNUM rnd; + BIGNUM *rng = NULL; + PyObject *ret, *tuple; + PyObject *format, *rangePyString; + char *randhex, *rangehex; + + /* Wow, it's a lot of work to convert into a hex string in C! */ + format = PyString_FromString("%x"); + if (!format) { + return NULL; + } + tuple = PyTuple_New(1); + if (!tuple) { + Py_DECREF(format); + PyErr_SetString(PyExc_RuntimeError, "PyTuple_New() fails"); + return NULL; + } + Py_INCREF(range); + PyTuple_SET_ITEM(tuple, 0, range); + rangePyString = PyString_Format(format, tuple); + if (!rangePyString) { + PyErr_SetString(PyExc_Exception, "PyString_Format failed"); + Py_DECREF(format); + Py_DECREF(tuple); + return NULL; + } + Py_DECREF(format); + Py_DECREF(tuple); + rangehex = PyString_AsString(rangePyString); + + if (!BN_hex2bn(&rng, rangehex)) { + /*Custom errors?*/ + PyErr_SetString(PyExc_Exception, ERR_reason_error_string(ERR_get_error())); + Py_DECREF(rangePyString); + return NULL; + } + + Py_DECREF(rangePyString); + + BN_init(&rnd); + + if (!BN_rand_range(&rnd, rng)) { + /*Custom errors?*/ + PyErr_SetString(PyExc_Exception, ERR_reason_error_string(ERR_get_error())); + BN_free(&rnd); + BN_free(rng); + return NULL; + } + + BN_free(rng); + + randhex = BN_bn2hex(&rnd); + if (!randhex) { + /*Custom errors?*/ + PyErr_SetString(PyExc_Exception, ERR_reason_error_string(ERR_get_error())); + BN_free(&rnd); + return NULL; + } + BN_free(&rnd); + + ret = PyLong_FromString(randhex, NULL, 16); + OPENSSL_free(randhex); + return ret; +} + +%} diff --git a/SWIG/_dh.i b/SWIG/_dh.i new file mode 100644 index 0000000..675b39f --- /dev/null +++ b/SWIG/_dh.i @@ -0,0 +1,186 @@ +/* Copyright (c) 1999 Ng Pheng Siong. All rights reserved. */ +/* $Id: _dh.i 695 2009-07-24 06:37:01Z heikki $ */ + +%{ +#include <openssl/bn.h> +#include <openssl/bio.h> +#include <openssl/err.h> +#include <openssl/pem.h> +#include <openssl/dh.h> +%} + +%apply Pointer NONNULL { DH * }; + +%rename(dh_new) DH_new; +extern DH *DH_new(void); +%rename(dh_free) DH_free; +extern void DH_free(DH *); +%rename(dh_size) DH_size; +extern int DH_size(const DH *); +%rename(dh_generate_key) DH_generate_key; +extern int DH_generate_key(DH *); +%rename(dhparams_print) DHparams_print; +%threadallow DHparams_print; +extern int DHparams_print(BIO *, const DH *); + +%constant int dh_check_ok = 0; +%constant int dh_check_p_not_prime = DH_CHECK_P_NOT_PRIME; +%constant int dh_check_p_not_strong = DH_CHECK_P_NOT_STRONG_PRIME; +%constant int dh_check_g_failed = DH_UNABLE_TO_CHECK_GENERATOR; +%constant int dh_check_bad_g = DH_NOT_SUITABLE_GENERATOR; + +%constant DH_GENERATOR_2 = 2; +%constant DH_GENERATOR_5 = 5; + +%inline %{ +static PyObject *_dh_err; + +void dh_init(PyObject *dh_err) { + Py_INCREF(dh_err); + _dh_err = dh_err; +} + +int dh_type_check(DH *dh) { + /* Our getting here means we passed Swig's type checking, + XXX Still need to check the pointer for sanity? */ + return 1; +} +%} + +%threadallow dh_read_parameters; +%inline %{ +DH *dh_read_parameters(BIO *bio) { + return PEM_read_bio_DHparams(bio, NULL, NULL, NULL); +} + +void gendh_callback(int p, int n, void *arg) { + PyObject *argv, *ret, *cbfunc; + + cbfunc = (PyObject *)arg; + argv = Py_BuildValue("(ii)", p, n); + ret = PyEval_CallObject(cbfunc, argv); + PyErr_Clear(); + Py_DECREF(argv); + Py_XDECREF(ret); +} + +DH *dh_generate_parameters(int plen, int g, PyObject *pyfunc) { + DH *dh; + + Py_INCREF(pyfunc); + dh = DH_generate_parameters(plen, g, gendh_callback, (void *)pyfunc); + Py_DECREF(pyfunc); + if (!dh) + PyErr_SetString(_dh_err, ERR_reason_error_string(ERR_get_error())); + return dh; +} + +/* Note return value shenanigan. */ +int dh_check(DH *dh) { + int err; + + return (DH_check(dh, &err)) ? 0 : err; +} + +PyObject *dh_compute_key(DH *dh, PyObject *pubkey) { + const void *pkbuf; + int pklen, klen; + void *key; + BIGNUM *pk; + PyObject *ret; + + if (m2_PyObject_AsReadBufferInt(pubkey, &pkbuf, &pklen) == -1) + return NULL; + + if (!(pk = BN_mpi2bn((unsigned char *)pkbuf, pklen, NULL))) { + PyErr_SetString(_dh_err, ERR_reason_error_string(ERR_get_error())); + return NULL; + } + if (!(key = PyMem_Malloc(DH_size(dh)))) { + BN_free(pk); + PyErr_SetString(PyExc_MemoryError, "dh_compute_key"); + return NULL; + } + if ((klen = DH_compute_key((unsigned char *)key, pk, dh)) == -1) { + BN_free(pk); + PyMem_Free(key); + PyErr_SetString(_dh_err, ERR_reason_error_string(ERR_get_error())); + return NULL; + } + ret = PyString_FromStringAndSize((const char *)key, klen); + BN_free(pk); + PyMem_Free(key); + return ret; +} + +PyObject *dh_get_p(DH *dh) { + if (!dh->p) { + PyErr_SetString(_dh_err, "'p' is unset"); + return NULL; + } + return bn_to_mpi(dh->p); +} + +PyObject *dh_get_g(DH *dh) { + if (!dh->g) { + PyErr_SetString(_dh_err, "'g' is unset"); + return NULL; + } + return bn_to_mpi(dh->g); +} + +PyObject *dh_get_pub(DH *dh) { + if (!dh->pub_key) { + PyErr_SetString(_dh_err, "'pub' is unset"); + return NULL; + } + return bn_to_mpi(dh->pub_key); +} + +PyObject *dh_get_priv(DH *dh) { + if (!dh->priv_key) { + PyErr_SetString(_dh_err, "'priv' is unset"); + return NULL; + } + return bn_to_mpi(dh->priv_key); +} + +PyObject *dh_set_p(DH *dh, PyObject *value) { + BIGNUM *bn; + const void *vbuf; + int vlen; + + if (m2_PyObject_AsReadBufferInt(value, &vbuf, &vlen) == -1) + return NULL; + + if (!(bn = BN_mpi2bn((unsigned char *)vbuf, vlen, NULL))) { + PyErr_SetString(_dh_err, ERR_reason_error_string(ERR_get_error())); + return NULL; + } + if (dh->p) + BN_free(dh->p); + dh->p = bn; + Py_INCREF(Py_None); + return Py_None; +} + +PyObject *dh_set_g(DH *dh, PyObject *value) { + BIGNUM *bn; + const void *vbuf; + int vlen; + + if (m2_PyObject_AsReadBufferInt(value, &vbuf, &vlen) == -1) + return NULL; + + if (!(bn = BN_mpi2bn((unsigned char *)vbuf, vlen, NULL))) { + PyErr_SetString(_dh_err, ERR_reason_error_string(ERR_get_error())); + return NULL; + } + if (dh->g) + BN_free(dh->g); + dh->g = bn; + Py_INCREF(Py_None); + return Py_None; +} +%} + diff --git a/SWIG/_dsa.i b/SWIG/_dsa.i new file mode 100644 index 0000000..addf33a --- /dev/null +++ b/SWIG/_dsa.i @@ -0,0 +1,349 @@ +/* Copyright (c) 1999-2000 Ng Pheng Siong. All rights reserved. */ +/* $Id: _dsa.i 723 2010-02-13 06:53:13Z heikki $ */ + +%{ +#include <openssl/bn.h> +#include <openssl/err.h> +#include <openssl/pem.h> +#include <openssl/dsa.h> + +PyObject *dsa_sig_get_r(DSA_SIG *dsa_sig) { + return bn_to_mpi(dsa_sig->r); +} + +PyObject *dsa_sig_get_s(DSA_SIG *dsa_sig) { + return bn_to_mpi(dsa_sig->s); +} +%} + +%apply Pointer NONNULL { DSA * }; + +%rename(dsa_new) DSA_new; +extern DSA *DSA_new(void); +%rename(dsa_free) DSA_free; +extern void DSA_free(DSA *); +%rename(dsa_size) DSA_size; +extern int DSA_size(const DSA *); /* assert(dsa->q); */ +%rename(dsa_gen_key) DSA_generate_key; +extern int DSA_generate_key(DSA *); + +%inline %{ +static PyObject *_dsa_err; + +void dsa_init(PyObject *dsa_err) { + Py_INCREF(dsa_err); + _dsa_err = dsa_err; +} + +void genparam_callback(int p, int n, void *arg) { + PyObject *argv, *ret, *cbfunc; + + cbfunc = (PyObject *)arg; + argv = Py_BuildValue("(ii)", p, n); + ret = PyEval_CallObject(cbfunc, argv); + PyErr_Clear(); + Py_DECREF(argv); + Py_XDECREF(ret); +} + +DSA *dsa_generate_parameters(int bits, PyObject *pyfunc) { + DSA *dsa; + + Py_INCREF(pyfunc); + dsa = DSA_generate_parameters(bits, NULL, 0, NULL, NULL, genparam_callback, (void *)pyfunc); + Py_DECREF(pyfunc); + if (!dsa) + PyErr_SetString(_dsa_err, ERR_reason_error_string(ERR_get_error())); + return dsa; +} + +PyObject *dsa_get_p(DSA *dsa) { + if (!dsa->p) { + PyErr_SetString(_dsa_err, "'p' is unset"); + return NULL; + } + return bn_to_mpi(dsa->p); +} + +PyObject *dsa_get_q(DSA *dsa) { + if (!dsa->q) { + PyErr_SetString(_dsa_err, "'q' is unset"); + return NULL; + } + return bn_to_mpi(dsa->q); +} + +PyObject *dsa_get_g(DSA *dsa) { + if (!dsa->g) { + PyErr_SetString(_dsa_err, "'g' is unset"); + return NULL; + } + return bn_to_mpi(dsa->g); +} + +PyObject *dsa_get_pub(DSA *dsa) { + if (!dsa->pub_key) { + PyErr_SetString(_dsa_err, "'pub' is unset"); + return NULL; + } + return bn_to_mpi(dsa->pub_key); +} + +PyObject *dsa_get_priv(DSA *dsa) { + if (!dsa->priv_key) { + PyErr_SetString(_dsa_err, "'priv' is unset"); + return NULL; + } + return bn_to_mpi(dsa->priv_key); +} + +PyObject *dsa_set_p(DSA *dsa, PyObject *value) { + BIGNUM *bn; + const void *vbuf; + int vlen; + + if (m2_PyObject_AsReadBufferInt(value, &vbuf, &vlen) == -1) + return NULL; + + if (!(bn = BN_mpi2bn((unsigned char *)vbuf, vlen, NULL))) { + PyErr_SetString(_dsa_err, ERR_reason_error_string(ERR_get_error())); + return NULL; + } + if (dsa->p) + BN_free(dsa->p); + dsa->p = bn; + Py_INCREF(Py_None); + return Py_None; +} + +PyObject *dsa_set_q(DSA *dsa, PyObject *value) { + BIGNUM *bn; + const void *vbuf; + int vlen; + + if (m2_PyObject_AsReadBufferInt(value, &vbuf, &vlen) == -1) + return NULL; + + if (!(bn = BN_mpi2bn((unsigned char *)vbuf, vlen, NULL))) { + PyErr_SetString(_dsa_err, ERR_reason_error_string(ERR_get_error())); + return NULL; + } + if (dsa->q) + BN_free(dsa->q); + dsa->q = bn; + Py_INCREF(Py_None); + return Py_None; +} + +PyObject *dsa_set_g(DSA *dsa, PyObject *value) { + BIGNUM *bn; + const void *vbuf; + int vlen; + + if (m2_PyObject_AsReadBufferInt(value, &vbuf, &vlen) == -1) + return NULL; + + if (!(bn = BN_mpi2bn((unsigned char *)vbuf, vlen, NULL))) { + PyErr_SetString(_dsa_err, ERR_reason_error_string(ERR_get_error())); + return NULL; + } + if (dsa->g) + BN_free(dsa->g); + dsa->g = bn; + Py_INCREF(Py_None); + return Py_None; +} +%} + +%inline %{ +DSA *dsa_read_params(BIO *f, PyObject *pyfunc) { + DSA *ret; + + Py_INCREF(pyfunc); + Py_BEGIN_ALLOW_THREADS + ret = PEM_read_bio_DSAparams(f, NULL, passphrase_callback, (void *)pyfunc); + Py_END_ALLOW_THREADS + Py_DECREF(pyfunc); + return ret; +} +%} + +%threadallow dsa_write_params_bio; +%inline %{ +int dsa_write_params_bio(DSA* dsa, BIO* f) { + return PEM_write_bio_DSAparams(f, dsa); +} +%} + +%inline %{ +int dsa_write_key_bio(DSA* dsa, BIO* f, EVP_CIPHER *cipher, PyObject *pyfunc) { + int ret; + + Py_INCREF(pyfunc); + Py_BEGIN_ALLOW_THREADS + ret = PEM_write_bio_DSAPrivateKey(f, dsa, cipher, NULL, 0, + passphrase_callback, (void *)pyfunc); + Py_END_ALLOW_THREADS + Py_DECREF(pyfunc); + return ret; +} +%} + +%inline %{ +int dsa_write_key_bio_no_cipher(DSA* dsa, BIO* f, PyObject *pyfunc) { + int ret; + + Py_INCREF(pyfunc); + Py_BEGIN_ALLOW_THREADS + ret = PEM_write_bio_DSAPrivateKey(f, dsa, NULL, NULL, 0, + passphrase_callback, (void *)pyfunc); + Py_END_ALLOW_THREADS + Py_DECREF(pyfunc); + return ret; +} +%} + +%threadallow dsa_write_pub_key_bio; +%inline %{ +int dsa_write_pub_key_bio(DSA* dsa, BIO* f) { + return PEM_write_bio_DSA_PUBKEY(f, dsa); +} +%} + +%inline %{ +DSA *dsa_read_key(BIO *f, PyObject *pyfunc) { + DSA *ret; + + Py_INCREF(pyfunc); + Py_BEGIN_ALLOW_THREADS + ret = PEM_read_bio_DSAPrivateKey(f, NULL, passphrase_callback, (void *)pyfunc); + Py_END_ALLOW_THREADS + Py_DECREF(pyfunc); + return ret; +} +%} + +%inline %{ +DSA *dsa_read_pub_key(BIO *f, PyObject *pyfunc) { + DSA *ret; + + Py_INCREF(pyfunc); + Py_BEGIN_ALLOW_THREADS + ret = PEM_read_bio_DSA_PUBKEY(f, NULL, passphrase_callback, (void *)pyfunc); + Py_END_ALLOW_THREADS + Py_DECREF(pyfunc); + return ret; +} + +PyObject *dsa_sign(DSA *dsa, PyObject *value) { + const void *vbuf; + int vlen; + PyObject *tuple; + DSA_SIG *sig; + + if (m2_PyObject_AsReadBufferInt(value, &vbuf, &vlen) == -1) + return NULL; + + if (!(sig = DSA_do_sign(vbuf, vlen, dsa))) { + PyErr_SetString(_dsa_err, ERR_reason_error_string(ERR_get_error())); + return NULL; + } + if (!(tuple = PyTuple_New(2))) { + DSA_SIG_free(sig); + PyErr_SetString(PyExc_RuntimeError, "PyTuple_New() fails"); + return NULL; + } + PyTuple_SET_ITEM(tuple, 0, dsa_sig_get_r(sig)); + PyTuple_SET_ITEM(tuple, 1, dsa_sig_get_s(sig)); + DSA_SIG_free(sig); + return tuple; +} + +int dsa_verify(DSA *dsa, PyObject *value, PyObject *r, PyObject *s) { + const void *vbuf, *rbuf, *sbuf; + int vlen, rlen, slen; + DSA_SIG *sig; + int ret; + + if ((m2_PyObject_AsReadBufferInt(value, &vbuf, &vlen) == -1) + || (m2_PyObject_AsReadBufferInt(r, &rbuf, &rlen) == -1) + || (m2_PyObject_AsReadBufferInt(s, &sbuf, &slen) == -1)) + return -1; + + if (!(sig = DSA_SIG_new())) { + PyErr_SetString(_dsa_err, ERR_reason_error_string(ERR_get_error())); + return -1; + } + if (!(sig->r = BN_mpi2bn((unsigned char *)rbuf, rlen, NULL))) { + PyErr_SetString(_dsa_err, ERR_reason_error_string(ERR_get_error())); + DSA_SIG_free(sig); + return -1; + } + if (!(sig->s = BN_mpi2bn((unsigned char *)sbuf, slen, NULL))) { + PyErr_SetString(_dsa_err, ERR_reason_error_string(ERR_get_error())); + DSA_SIG_free(sig); + return -1; + } + ret = DSA_do_verify(vbuf, vlen, sig, dsa); + DSA_SIG_free(sig); + if (ret == -1) + PyErr_SetString(_dsa_err, ERR_reason_error_string(ERR_get_error())); + return ret; +} + +PyObject *dsa_sign_asn1(DSA *dsa, PyObject *value) { + const void *vbuf; + int vlen; + void *sigbuf; + unsigned int siglen; + PyObject *ret; + + if (m2_PyObject_AsReadBufferInt(value, &vbuf, &vlen) == -1) + return NULL; + + if (!(sigbuf = PyMem_Malloc(DSA_size(dsa)))) { + PyErr_SetString(PyExc_MemoryError, "dsa_sign_asn1"); + return NULL; + } + if (!DSA_sign(0, vbuf, vlen, (unsigned char *)sigbuf, &siglen, dsa)) { + PyErr_SetString(_dsa_err, ERR_reason_error_string(ERR_get_error())); + PyMem_Free(sigbuf); + return NULL; + } + ret = PyString_FromStringAndSize(sigbuf, siglen); + PyMem_Free(sigbuf); + return ret; +} + +int dsa_verify_asn1(DSA *dsa, PyObject *value, PyObject *sig) { + const void *vbuf; + void *sbuf; + int vlen, slen, ret; + + if ((m2_PyObject_AsReadBufferInt(value, &vbuf, &vlen) == -1) + || (m2_PyObject_AsReadBufferInt(sig, (const void **)&sbuf, &slen) + == -1)) + return -1; + + if ((ret = DSA_verify(0, vbuf, vlen, sbuf, slen, dsa)) == -1) + PyErr_SetString(_dsa_err, ERR_reason_error_string(ERR_get_error())); + return ret; +} + +int dsa_check_key(DSA *dsa) { + return (dsa->pub_key) && (dsa->priv_key); +} + +int dsa_check_pub_key(DSA *dsa) { + return dsa->pub_key ? 1 : 0; +} + +int dsa_keylen(DSA *dsa) { + return BN_num_bits(dsa->p); +} + +int dsa_type_check(DSA *dsa) { + return 1; +} +%} + diff --git a/SWIG/_ec.i b/SWIG/_ec.i new file mode 100644 index 0000000..f0e52bd --- /dev/null +++ b/SWIG/_ec.i @@ -0,0 +1,419 @@ +/* Copyright (c) 1999-2000 Ng Pheng Siong. All rights reserved. + Portions copyright (c) 2005-2006 Vrije Universiteit Amsterdam. All rights reserved. + + Most code originally from _dsa.i, _rsa.i and _dh.i and adjusted for EC use. +*/ + +%include <openssl/opensslconf.h> + +#if OPENSSL_VERSION_NUMBER < 0x0090800fL || defined(OPENSSL_NO_EC) +#undef OPENSSL_NO_EC +%constant OPENSSL_NO_EC = 1; +#else +%constant OPENSSL_NO_EC = 0; + +%{ +#include <openssl/bn.h> +#include <openssl/err.h> +#include <openssl/pem.h> +#include <openssl/x509.h> +#include <openssl/ecdsa.h> +#include <openssl/ecdh.h> +%} + +%apply Pointer NONNULL { EC_KEY * }; + +%rename(ec_key_new) EC_KEY_new; +extern EC_KEY *EC_KEY_new(void); +%rename(ec_key_free) EC_KEY_free; +extern void EC_KEY_free(EC_KEY *); +%rename(ec_key_size) ECDSA_size; +extern int ECDSA_size(const EC_KEY *); +%rename(ec_key_gen_key) EC_KEY_generate_key; +extern int EC_KEY_generate_key(EC_KEY *); +%rename(ec_key_check_key) EC_KEY_check_key; +extern int EC_KEY_check_key(const EC_KEY *); + +/* Curve identifier constants from OpenSSL */ +%constant int NID_secp112r1 = NID_secp112r1; +%constant int NID_secp112r2 = NID_secp112r2; +%constant int NID_secp128r1 = NID_secp128r1; +%constant int NID_secp128r2 = NID_secp128r2; +%constant int NID_secp160k1 = NID_secp160k1; +%constant int NID_secp160r1 = NID_secp160r1; +%constant int NID_secp160r2 = NID_secp160r2; +%constant int NID_secp192k1 = NID_secp192k1; +%constant int NID_secp224k1 = NID_secp224k1; +%constant int NID_secp224r1 = NID_secp224r1; +%constant int NID_secp256k1 = NID_secp256k1; +%constant int NID_secp384r1 = NID_secp384r1; +%constant int NID_secp521r1 = NID_secp521r1; +%constant int NID_sect113r1 = NID_sect113r1; +%constant int NID_sect113r2 = NID_sect113r2; +%constant int NID_sect131r1 = NID_sect131r1; +%constant int NID_sect131r2 = NID_sect131r2; +%constant int NID_sect163k1 = NID_sect163k1; +%constant int NID_sect163r1 = NID_sect163r1; +%constant int NID_sect163r2 = NID_sect163r2; +%constant int NID_sect193r1 = NID_sect193r1; +%constant int NID_sect193r2 = NID_sect193r2; +%constant int NID_sect233k1 = NID_sect233k1; +%constant int NID_sect233r1 = NID_sect233r1; +%constant int NID_sect239k1 = NID_sect239k1; +%constant int NID_sect283k1 = NID_sect283k1; +%constant int NID_sect283r1 = NID_sect283r1; +%constant int NID_sect409k1 = NID_sect409k1; +%constant int NID_sect409r1 = NID_sect409r1; +%constant int NID_sect571k1 = NID_sect571k1; +%constant int NID_sect571r1 = NID_sect571r1; + +%constant int NID_X9_62_prime192v1 = NID_X9_62_prime192v1; +%constant int NID_X9_62_prime192v2 = NID_X9_62_prime192v2; +%constant int NID_X9_62_prime192v3 = NID_X9_62_prime192v3; +%constant int NID_X9_62_prime239v1 = NID_X9_62_prime239v1; +%constant int NID_X9_62_prime239v2 = NID_X9_62_prime239v2; +%constant int NID_X9_62_prime239v3 = NID_X9_62_prime239v3; +%constant int NID_X9_62_prime256v1 = NID_X9_62_prime256v1; +%constant int NID_X9_62_c2pnb163v1 = NID_X9_62_c2pnb163v1; +%constant int NID_X9_62_c2pnb163v2 = NID_X9_62_c2pnb163v2; +%constant int NID_X9_62_c2pnb163v3 = NID_X9_62_c2pnb163v3; +%constant int NID_X9_62_c2pnb176v1 = NID_X9_62_c2pnb176v1; +%constant int NID_X9_62_c2tnb191v1 = NID_X9_62_c2tnb191v1; +%constant int NID_X9_62_c2tnb191v2 = NID_X9_62_c2tnb191v2; +%constant int NID_X9_62_c2tnb191v3 = NID_X9_62_c2tnb191v3; +%constant int NID_X9_62_c2pnb208w1 = NID_X9_62_c2pnb208w1; +%constant int NID_X9_62_c2tnb239v1 = NID_X9_62_c2tnb239v1; +%constant int NID_X9_62_c2tnb239v2 = NID_X9_62_c2tnb239v2; +%constant int NID_X9_62_c2tnb239v3 = NID_X9_62_c2tnb239v3; +%constant int NID_X9_62_c2pnb272w1 = NID_X9_62_c2pnb272w1; +%constant int NID_X9_62_c2pnb304w1 = NID_X9_62_c2pnb304w1; +%constant int NID_X9_62_c2tnb359v1 = NID_X9_62_c2tnb359v1; +%constant int NID_X9_62_c2pnb368w1 = NID_X9_62_c2pnb368w1; +%constant int NID_X9_62_c2tnb431r1 = NID_X9_62_c2tnb431r1; + +%constant int NID_wap_wsg_idm_ecid_wtls1 = NID_wap_wsg_idm_ecid_wtls1; +%constant int NID_wap_wsg_idm_ecid_wtls3 = NID_wap_wsg_idm_ecid_wtls3; +%constant int NID_wap_wsg_idm_ecid_wtls4 = NID_wap_wsg_idm_ecid_wtls4; +%constant int NID_wap_wsg_idm_ecid_wtls5 = NID_wap_wsg_idm_ecid_wtls5; +%constant int NID_wap_wsg_idm_ecid_wtls6 = NID_wap_wsg_idm_ecid_wtls6; +%constant int NID_wap_wsg_idm_ecid_wtls7 = NID_wap_wsg_idm_ecid_wtls7; +%constant int NID_wap_wsg_idm_ecid_wtls8 = NID_wap_wsg_idm_ecid_wtls8; +%constant int NID_wap_wsg_idm_ecid_wtls9 = NID_wap_wsg_idm_ecid_wtls9; +%constant int NID_wap_wsg_idm_ecid_wtls10 = NID_wap_wsg_idm_ecid_wtls10; +%constant int NID_wap_wsg_idm_ecid_wtls11 = NID_wap_wsg_idm_ecid_wtls11; +%constant int NID_wap_wsg_idm_ecid_wtls12 = NID_wap_wsg_idm_ecid_wtls12; + +%constant int NID_ipsec3 = NID_ipsec3; +%constant int NID_ipsec4 = NID_ipsec4; + + +%inline %{ +static PyObject *_ec_err; + +void ec_init(PyObject *ec_err) { + Py_INCREF(ec_err); + _ec_err = ec_err; +} + +EC_KEY* ec_key_new_by_curve_name(int nid) +{ + EC_KEY *key; + EC_GROUP *group; + int ret =0; + point_conversion_form_t form = POINT_CONVERSION_UNCOMPRESSED; + int asn1_flag = OPENSSL_EC_NAMED_CURVE; + + /* If I simply do "return EC_KEY_new_by_curve_name(nid);" + * I get large public keys (222 vs 84 bytes for sect233k1 curve). + * I don't know why that is, but 'openssl ecparam -genkey ...' sets + * the ASN.1 flag and the point conversion form, and gets the + * small pub keys. So let's do that too. + */ + key = EC_KEY_new(); + if (!key) { + PyErr_SetString(PyExc_MemoryError, "ec_key_new_by_curve_name"); + return NULL; + } + group = EC_GROUP_new_by_curve_name(nid); + if (!group) { + EC_KEY_free(key); + PyErr_SetString(_ec_err, ERR_reason_error_string(ERR_get_error())); + return NULL; + } + EC_GROUP_set_asn1_flag(group, asn1_flag); + EC_GROUP_set_point_conversion_form(group, form); + ret = EC_KEY_set_group(key, group); + EC_GROUP_free(group); + if (ret == 0) + { + /* EC_KEY_set_group only returns 0 or 1, and does not set error. */ + PyErr_SetString(_ec_err, "cannot set key's group"); + EC_KEY_free(key); + return NULL; + } + + return key; +} + +PyObject *ec_key_get_public_der(EC_KEY *key) { + + unsigned char *src=NULL; + void *dst=NULL; + int src_len=0; + Py_ssize_t dst_len=0; + PyObject *pyo=NULL; + int ret=0; + + /* Convert to binary */ + src_len = i2d_EC_PUBKEY( key, &src ); + if (src_len < 0) + { + PyErr_SetString(_ec_err, ERR_reason_error_string(ERR_get_error())); + return NULL; + } + /* Create a PyBuffer containing a copy of the binary, + * to simplify memory deallocation + */ + pyo = PyBuffer_New( src_len ); + ret = PyObject_AsWriteBuffer( pyo, &dst, &dst_len ); + assert( src_len == dst_len ); + if (ret < 0) + { + Py_DECREF(pyo); + OPENSSL_free(src); + PyErr_SetString(_ec_err, "cannot get write buffer"); + return NULL; + } + memcpy( dst, src, src_len ); + OPENSSL_free(src); + + return pyo; +} +%} + +%threadallow ec_key_read_pubkey; +%inline %{ +EC_KEY *ec_key_read_pubkey(BIO *f) { + return PEM_read_bio_EC_PUBKEY(f, NULL, NULL, NULL); +} +%} + +%threadallow ec_key_write_pubkey; +%inline %{ +int ec_key_write_pubkey(EC_KEY *key, BIO *f) { + return PEM_write_bio_EC_PUBKEY(f, key ); +} +%} + +%inline %{ +EC_KEY *ec_key_read_bio(BIO *f, PyObject *pyfunc) { + EC_KEY *ret; + + Py_INCREF(pyfunc); + Py_BEGIN_ALLOW_THREADS + ret = PEM_read_bio_ECPrivateKey(f, NULL, passphrase_callback, (void *)pyfunc); + Py_END_ALLOW_THREADS + Py_DECREF(pyfunc); + return ret; +} +%} + +%inline %{ +int ec_key_write_bio(EC_KEY *key, BIO *f, EVP_CIPHER *cipher, PyObject *pyfunc) { + int ret; + + Py_INCREF(pyfunc); + Py_BEGIN_ALLOW_THREADS + ret = PEM_write_bio_ECPrivateKey(f, key, cipher, NULL, 0, + passphrase_callback, (void *)pyfunc); + Py_END_ALLOW_THREADS + Py_DECREF(pyfunc); + return ret; +} +%} + +%inline %{ +int ec_key_write_bio_no_cipher(EC_KEY *key, BIO *f, PyObject *pyfunc) { + int ret; + + Py_INCREF(pyfunc); + Py_BEGIN_ALLOW_THREADS + ret = PEM_write_bio_ECPrivateKey(f, key, NULL, NULL, 0, + passphrase_callback, (void *)pyfunc); + Py_END_ALLOW_THREADS + Py_DECREF(pyfunc); + return ret; +} + + +PyObject *ecdsa_sig_get_r(ECDSA_SIG *ecdsa_sig) { + return bn_to_mpi(ecdsa_sig->r); +} + +PyObject *ecdsa_sig_get_s(ECDSA_SIG *ecdsa_sig) { + return bn_to_mpi(ecdsa_sig->s); +} + +PyObject *ecdsa_sign(EC_KEY *key, PyObject *value) { + const void *vbuf; + int vlen; + PyObject *tuple; + ECDSA_SIG *sig; + + if (m2_PyObject_AsReadBufferInt(value, &vbuf, &vlen) == -1) + return NULL; + + if (!(sig = ECDSA_do_sign(vbuf, vlen, key))) { + PyErr_SetString(_ec_err, ERR_reason_error_string(ERR_get_error())); + return NULL; + } + if (!(tuple = PyTuple_New(2))) { + ECDSA_SIG_free(sig); + PyErr_SetString(PyExc_RuntimeError, "PyTuple_New() fails"); + return NULL; + } + PyTuple_SET_ITEM(tuple, 0, ecdsa_sig_get_r(sig)); + PyTuple_SET_ITEM(tuple, 1, ecdsa_sig_get_s(sig)); + ECDSA_SIG_free(sig); + return tuple; +} + +int ecdsa_verify(EC_KEY *key, PyObject *value, PyObject *r, PyObject *s) { + const void *vbuf, *rbuf, *sbuf; + int vlen, rlen, slen; + ECDSA_SIG *sig; + int ret; + + if ((m2_PyObject_AsReadBufferInt(value, &vbuf, &vlen) == -1) + || (m2_PyObject_AsReadBufferInt(r, &rbuf, &rlen) == -1) + || (m2_PyObject_AsReadBufferInt(s, &sbuf, &slen) == -1)) + return -1; + + if (!(sig = ECDSA_SIG_new())) { + PyErr_SetString(_ec_err, ERR_reason_error_string(ERR_get_error())); + return -1; + } + if (!BN_mpi2bn((unsigned char *)rbuf, rlen, sig->r)) { + PyErr_SetString(_ec_err, ERR_reason_error_string(ERR_get_error())); + ECDSA_SIG_free(sig); + return -1; + } + if (!BN_mpi2bn((unsigned char *)sbuf, slen, sig->s)) { + PyErr_SetString(_ec_err, ERR_reason_error_string(ERR_get_error())); + ECDSA_SIG_free(sig); + return -1; + } + ret = ECDSA_do_verify(vbuf, vlen, sig, key); + ECDSA_SIG_free(sig); + if (ret == -1) + PyErr_SetString(_ec_err, ERR_reason_error_string(ERR_get_error())); + return ret; +} + + +PyObject *ecdsa_sign_asn1(EC_KEY *key, PyObject *value) { + const void *vbuf; + int vlen; + void *sigbuf; + unsigned int siglen; + PyObject *ret; + + if (m2_PyObject_AsReadBufferInt(value, &vbuf, &vlen) == -1) + return NULL; + + if (!(sigbuf = PyMem_Malloc(ECDSA_size(key)))) { + PyErr_SetString(PyExc_MemoryError, "ecdsa_sign_asn1"); + return NULL; + } + if (!ECDSA_sign(0, vbuf, vlen, (unsigned char *)sigbuf, &siglen, key)) { + PyErr_SetString(_ec_err, ERR_reason_error_string(ERR_get_error())); + PyMem_Free(sigbuf); + return NULL; + } + ret = PyString_FromStringAndSize(sigbuf, siglen); + PyMem_Free(sigbuf); + return ret; +} + + +int ecdsa_verify_asn1(EC_KEY *key, PyObject *value, PyObject *sig) { + const void *vbuf; + void *sbuf; + int vlen, slen, ret; + + if ((m2_PyObject_AsReadBufferInt(value, &vbuf, &vlen) == -1) + || (m2_PyObject_AsReadBufferInt(sig, (const void **)&sbuf, &slen) + == -1)) + return -1; + + if ((ret = ECDSA_verify(0, vbuf, vlen, sbuf, slen, key)) == -1) + PyErr_SetString(_ec_err, ERR_reason_error_string(ERR_get_error())); + return ret; +} + +PyObject *ecdh_compute_key(EC_KEY *keypairA, EC_KEY *pubkeyB) { + int sharedkeylen; + void *sharedkey; + const EC_POINT *pkpointB; + PyObject *ret; + const EC_GROUP* groupA; + + if ((pkpointB = EC_KEY_get0_public_key(pubkeyB)) == NULL) + { + PyErr_SetString(_ec_err, ERR_reason_error_string(ERR_get_error())); + return NULL; + } + + groupA = EC_KEY_get0_group(keypairA); + sharedkeylen = (EC_GROUP_get_degree(groupA) + 7)/8; + + if (!(sharedkey = PyMem_Malloc(sharedkeylen))) { + PyErr_SetString(PyExc_MemoryError, "ecdh_compute_key"); + return NULL; + } + if ((sharedkeylen = ECDH_compute_key((unsigned char *)sharedkey, sharedkeylen, pkpointB, keypairA, NULL)) == -1) { + PyMem_Free(sharedkey); + PyErr_SetString(_ec_err, ERR_reason_error_string(ERR_get_error())); + return NULL; + } + + ret = PyString_FromStringAndSize((const char *)sharedkey, sharedkeylen); + PyMem_Free(sharedkey); + + return ret; +} + + +EC_KEY* ec_key_from_pubkey_der(PyObject *pubkey) { + const void *keypairbuf; + Py_ssize_t keypairbuflen; + const unsigned char *tempBuf; + EC_KEY *keypair; + + if (PyObject_AsReadBuffer(pubkey, &keypairbuf, &keypairbuflen) == -1) + { + return NULL; + } + + tempBuf = (const unsigned char *)keypairbuf; + if ((keypair = d2i_EC_PUBKEY( NULL, &tempBuf, keypairbuflen)) == 0) + { + PyErr_SetString(_ec_err, ERR_reason_error_string(ERR_get_error())); + return NULL; + } + return keypair; +} + + +// According to [SEC2] the degree of the group is defined as EC key length +int ec_key_keylen(EC_KEY *key) { + const EC_GROUP *group = EC_KEY_get0_group(key); + return EC_GROUP_get_degree(group); +} + +int ec_key_type_check(EC_KEY *key) { + return 1; +} +%} +#endif // if OpenSSL version with EC support + diff --git a/SWIG/_engine.i b/SWIG/_engine.i new file mode 100644 index 0000000..b55720f --- /dev/null +++ b/SWIG/_engine.i @@ -0,0 +1,207 @@ +/* + * -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * vim: syntax=c sts=4 sw=4 + * + * ENGINE functions from engine(3SSL). + * + * Pavel Shramov + * IMEC MSU + */ +%{ +#include <openssl/engine.h> +#include <openssl/ui.h> +#include <stdio.h> +%} + +%apply Pointer NONNULL { ENGINE * }; +%apply Pointer NONNULL { const ENGINE * }; +%apply Pointer NONNULL { const char * }; + +/* + * Functions to load different engines + */ +%rename(engine_load_builtin_engines) ENGINE_load_builtin_engines; +extern void ENGINE_load_builtin_engines(void); + +%rename(engine_load_dynamic) ENGINE_load_dynamic; +extern void ENGINE_load_dynamic(void); + +%rename(engine_load_openssl) ENGINE_load_openssl; +extern void ENGINE_load_openssl(void); + +%rename(engine_cleanup) ENGINE_cleanup; +extern void ENGINE_cleanup(void); + +/* + * Engine allocation functions + */ +%rename(engine_new) ENGINE_new; +extern ENGINE * ENGINE_new(); + +%rename(engine_by_id) ENGINE_by_id; +extern ENGINE * ENGINE_by_id(const char *); + +%rename(engine_free) ENGINE_free; +extern int ENGINE_free(ENGINE *); + +%rename(engine_init) ENGINE_init; +extern int ENGINE_init(ENGINE *); + +%rename(engine_finish) ENGINE_finish; +extern int ENGINE_finish(ENGINE *); + +/* + * Engine id/name functions + */ +%rename(engine_get_id) ENGINE_get_id; +extern const char * ENGINE_get_id(const ENGINE *); + +%rename(engine_get_name) ENGINE_get_name; +extern const char * ENGINE_get_name(const ENGINE *); + +/* + * Engine control functions + * Control argument may be NULL (e.g for LOAD command) + */ +%clear const char *; +%rename(engine_ctrl_cmd_string) ENGINE_ctrl_cmd_string; +extern int ENGINE_ctrl_cmd_string(ENGINE *e, const char *NONNULL, + const char *arg, int cmd_optional); + +%apply Pointer NONNULL { const char * }; + +/* + * UI methods. + * XXX: UI_OpenSSL method is static and UI_destroy_method is not needed. + */ +%rename(ui_openssl) UI_OpenSSL; +extern UI_METHOD * UI_OpenSSL(); + +/* +%rename(ui_destroy_method) UI_destroy_method; +extern void UI_destroy_method(UI_METHOD *ui_method); + */ + +%clear const char *; +%inline %{ + +/* + * Code from engine-pkcs11 1.4.0 in engine-pkcs11.c + * + +99 static char *get_pin(UI_METHOD * ui_method, void *callback_data, char *sc_pin, +100 int maxlen) +101 { +102 UI *ui; +103 struct { +104 const void *password; +105 const char *prompt_info; +106 } *mycb = callback_data; +107 +108 if (mycb->password) { +109 sc_pin = set_pin(mycb->password); +110 return sc_pin; +111 } + + * + * So callback_data need to be always provided and have fixed type. + * UI method still may be NULL. + * + * Following functions allocate and free callback data structure with + * optional password set. + */ + +typedef struct { + char * password; + char * prompt; +} _cbd_t; + +void * engine_pkcs11_data_new(const char *pin) { + _cbd_t * cb = (_cbd_t *) PyMem_Malloc(sizeof(_cbd_t)); + if (!cb) { + PyErr_SetString(PyExc_MemoryError, "engine_pkcs11_data_new"); + return NULL; + } + cb->password = NULL; + if (pin) { + size_t size = strlen(pin); + cb->password = (char *) PyMem_Malloc(size + 1); + if (!cb->password) { + PyErr_SetString(PyExc_MemoryError, "engine_pkcs11_data_new"); + PyMem_Free(cb); + return NULL; + } + memcpy(cb->password, pin, size + 1); + } + cb->prompt = NULL; + return cb; +} + +void engine_pkcs11_data_free(void * vcb) { + _cbd_t * cb = (_cbd_t *) vcb; + if (!cb) + return; + if (cb->password) + PyMem_Free(cb->password); + PyMem_Free(cb); +} + +%} +%apply Pointer NONNULL { const char * }; + +/* + * Engine key/cert load functions. + * See above notice about callback_data. + */ +%rename(engine_load_private_key) ENGINE_load_private_key; +extern EVP_PKEY *ENGINE_load_private_key(ENGINE *e, const char *key_id, + UI_METHOD *ui_method, void *callback_data); +%rename(engine_load_public_key) ENGINE_load_public_key; +extern EVP_PKEY *ENGINE_load_public_key(ENGINE *e, const char *key_id, + UI_METHOD *ui_method, void *callback_data); + +/* + * This function may be not implemented in engine. + * pkcs11 engine has this control. + */ +%inline %{ +static PyObject *_engine_err; + +void engine_init_error(PyObject *engine_err) { + Py_INCREF(engine_err); + _engine_err = engine_err; +} + +X509 * engine_load_certificate(ENGINE *e, const char * slot) { + struct { + const char * slot; + X509 * cert; + } cbd; + cbd.slot = slot; + cbd.cert = NULL; + if (!ENGINE_ctrl_cmd(e, "LOAD_CERT_CTRL", 0, &cbd, NULL, 0)) { + PyErr_SetString(_engine_err, "cannot load certificate"); + return NULL; + } + return cbd.cert; +} +%} + +/* These flags are used to control combinations of algorithm (methods) + * by bitwise "OR"ing. */ +%constant int ENGINE_METHOD_RSA = 0x0001; +%constant int ENGINE_METHOD_DSA = 0x0002; +%constant int ENGINE_METHOD_DH = 0x0004; +%constant int ENGINE_METHOD_RAND = 0x0008; +%constant int ENGINE_METHOD_ECDH = 0x0010; +%constant int ENGINE_METHOD_ECDSA = 0x0020; +%constant int ENGINE_METHOD_CIPHERS = 0x0040; +%constant int ENGINE_METHOD_DIGESTS = 0x0080; +%constant int ENGINE_METHOD_STORE = 0x0100; +/* Obvious all-or-nothing cases. */ +%constant int ENGINE_METHOD_ALL = 0xFFFF; +%constant int ENGINE_METHOD_NONE = 0x0000; + +%rename(engine_set_default) ENGINE_set_default; +extern int ENGINE_set_default(ENGINE *e, unsigned int flags); + diff --git a/SWIG/_evp.i b/SWIG/_evp.i new file mode 100644 index 0000000..0593eed --- /dev/null +++ b/SWIG/_evp.i @@ -0,0 +1,596 @@ +/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* +Copyright (c) 1999 Ng Pheng Siong. All rights reserved. + +Portions Copyright (c) 2004-2007 Open Source Applications Foundation. +Author: Heikki Toivonen + +Copyright (c) 2009-2010 Heikki Toivonen. All rights reserved. + +*/ + +%include <openssl/opensslconf.h> + +%{ +#include <assert.h> +#include <openssl/err.h> +#include <openssl/evp.h> +#include <openssl/hmac.h> +#include <openssl/rsa.h> +#include <openssl/opensslv.h> +%} + +%apply Pointer NONNULL { EVP_MD_CTX * }; +%apply Pointer NONNULL { EVP_MD * }; +%apply Pointer NONNULL { EVP_PKEY * }; +%apply Pointer NONNULL { HMAC_CTX * }; +%apply Pointer NONNULL { EVP_CIPHER_CTX * }; +%apply Pointer NONNULL { EVP_CIPHER * }; +%apply Pointer NONNULL { RSA * }; + +%rename(md5) EVP_md5; +extern const EVP_MD *EVP_md5(void); +%rename(sha1) EVP_sha1; +extern const EVP_MD *EVP_sha1(void); +%rename(ripemd160) EVP_ripemd160; +extern const EVP_MD *EVP_ripemd160(void); + +#if OPENSSL_VERSION_NUMBER >= 0x0090800fL +%rename(sha224) EVP_sha224; +extern const EVP_MD *EVP_sha224(void); +%rename(sha256) EVP_sha256; +extern const EVP_MD *EVP_sha256(void); +%rename(sha384) EVP_sha384; +extern const EVP_MD *EVP_sha384(void); +%rename(sha512) EVP_sha512; +extern const EVP_MD *EVP_sha512(void); +#endif + +%rename(digest_init) EVP_DigestInit; +extern int EVP_DigestInit(EVP_MD_CTX *, const EVP_MD *); + +%rename(des_ecb) EVP_des_ecb; +extern const EVP_CIPHER *EVP_des_ecb(void); +%rename(des_ede_ecb) EVP_des_ede; +extern const EVP_CIPHER *EVP_des_ede(void); +%rename(des_ede3_ecb) EVP_des_ede3; +extern const EVP_CIPHER *EVP_des_ede3(void); +%rename(des_cbc) EVP_des_cbc; +extern const EVP_CIPHER *EVP_des_cbc(void); +%rename(des_ede_cbc) EVP_des_ede_cbc; +extern const EVP_CIPHER *EVP_des_ede_cbc(void); +%rename(des_ede3_cbc) EVP_des_ede3_cbc; +extern const EVP_CIPHER *EVP_des_ede3_cbc(void); +%rename(des_cfb) EVP_des_cfb; +extern const EVP_CIPHER *EVP_des_cfb(void); +%rename(des_ede_cfb) EVP_des_ede_cfb; +extern const EVP_CIPHER *EVP_des_ede_cfb(void); +%rename(des_ede3_cfb) EVP_des_ede3_cfb; +extern const EVP_CIPHER *EVP_des_ede3_cfb(void); +%rename(des_ofb) EVP_des_ofb; +extern const EVP_CIPHER *EVP_des_ofb(void); +%rename(des_ede_ofb) EVP_des_ede_ofb; +extern const EVP_CIPHER *EVP_des_ede_ofb(void); +%rename(des_ede3_ofb) EVP_des_ede3_ofb; +extern const EVP_CIPHER *EVP_des_ede3_ofb(void); +%rename(bf_ecb) EVP_bf_ecb; +extern const EVP_CIPHER *EVP_bf_ecb(void); +%rename(bf_cbc) EVP_bf_cbc; +extern const EVP_CIPHER *EVP_bf_cbc(void); +%rename(bf_cfb) EVP_bf_cfb; +extern const EVP_CIPHER *EVP_bf_cfb(void); +%rename(bf_ofb) EVP_bf_ofb; +extern const EVP_CIPHER *EVP_bf_ofb(void); +/* +%rename(idea_ecb) extern const EVP_CIPHER *EVP_idea_ecb(void); +%rename(idea_cbc) extern const EVP_CIPHER *EVP_idea_cbc(void); +%rename(idea_cfb) extern const EVP_CIPHER *EVP_idea_cfb(void); +%rename(idea_ofb) extern const EVP_CIPHER *EVP_idea_ofb(void); +*/ +%rename(cast5_ecb) EVP_cast5_ecb; +extern const EVP_CIPHER *EVP_cast5_ecb(void); +%rename(cast5_cbc) EVP_cast5_cbc; +extern const EVP_CIPHER *EVP_cast5_cbc(void); +%rename(cast5_cfb) EVP_cast5_cfb; +extern const EVP_CIPHER *EVP_cast5_cfb(void); +%rename(cast5_ofb) EVP_cast5_ofb; +extern const EVP_CIPHER *EVP_cast5_ofb(void); +/* +%rename(rc5_ecb) extern const EVP_CIPHER *EVP_rc5_32_12_16_ecb(void); +%rename(rc5_cbc) extern const EVP_CIPHER *EVP_rc5_32_12_16_cbc(void); +%rename(rc5_cfb) extern const EVP_CIPHER *EVP_rc5_32_12_16_cfb(void); +%rename(rc5_ofb) extern const EVP_CIPHER *EVP_rc5_32_12_16_ofb(void); +*/ +%rename(rc4) EVP_rc4; +extern const EVP_CIPHER *EVP_rc4(void); +%rename(rc2_40_cbc) EVP_rc2_40_cbc; +extern const EVP_CIPHER *EVP_rc2_40_cbc(void); +%rename(aes_128_ecb) EVP_aes_128_ecb; +extern const EVP_CIPHER *EVP_aes_128_ecb(void); +%rename(aes_128_cbc) EVP_aes_128_cbc; +extern const EVP_CIPHER *EVP_aes_128_cbc(void); +%rename(aes_128_cfb) EVP_aes_128_cfb; +extern const EVP_CIPHER *EVP_aes_128_cfb(void); +%rename(aes_128_ofb) EVP_aes_128_ofb; +extern const EVP_CIPHER *EVP_aes_128_ofb(void); +%rename(aes_192_ecb) EVP_aes_192_ecb; +extern const EVP_CIPHER *EVP_aes_192_ecb(void); +%rename(aes_192_cbc) EVP_aes_192_cbc; +extern const EVP_CIPHER *EVP_aes_192_cbc(void); +%rename(aes_192_cfb) EVP_aes_192_cfb; +extern const EVP_CIPHER *EVP_aes_192_cfb(void); +%rename(aes_192_ofb) EVP_aes_192_ofb; +extern const EVP_CIPHER *EVP_aes_192_ofb(void); +%rename(aes_256_ecb) EVP_aes_256_ecb; +extern const EVP_CIPHER *EVP_aes_256_ecb(void); +%rename(aes_256_cbc) EVP_aes_256_cbc; +extern const EVP_CIPHER *EVP_aes_256_cbc(void); +%rename(aes_256_cfb) EVP_aes_256_cfb; +extern const EVP_CIPHER *EVP_aes_256_cfb(void); +%rename(aes_256_ofb) EVP_aes_256_ofb; +extern const EVP_CIPHER *EVP_aes_256_ofb(void); + +%rename(cipher_set_padding) EVP_CIPHER_CTX_set_padding; +extern int EVP_CIPHER_CTX_set_padding(EVP_CIPHER_CTX *, int); + +%rename(pkey_new) EVP_PKEY_new; +extern EVP_PKEY *EVP_PKEY_new(void); +%rename(pkey_free) EVP_PKEY_free; +extern void EVP_PKEY_free(EVP_PKEY *); +%rename(pkey_assign) EVP_PKEY_assign; +extern int EVP_PKEY_assign(EVP_PKEY *, int, char *); +#if OPENSSL_VERSION_NUMBER >= 0x0090800fL && !defined(OPENSSL_NO_EC) +%rename(pkey_assign_ec) EVP_PKEY_assign_EC_KEY; +extern int EVP_PKEY_assign_EC_KEY(EVP_PKEY *, EC_KEY *); +#endif +%rename(pkey_set1_rsa) EVP_PKEY_set1_RSA; +extern int EVP_PKEY_set1_RSA(EVP_PKEY *, RSA *); +%rename(pkey_get1_rsa) EVP_PKEY_get1_RSA; +extern RSA* EVP_PKEY_get1_RSA(EVP_PKEY *); +%rename(sign_init) EVP_SignInit; +extern int EVP_SignInit(EVP_MD_CTX *, const EVP_MD *); +%rename(verify_init) EVP_VerifyInit; +extern int EVP_VerifyInit(EVP_MD_CTX *, const EVP_MD *); +%rename(pkey_size) EVP_PKEY_size; +extern int EVP_PKEY_size(EVP_PKEY *); + +%inline %{ +#define PKCS5_SALT_LEN 8 + +static PyObject *_evp_err; + +void evp_init(PyObject *evp_err) { + Py_INCREF(evp_err); + _evp_err = evp_err; +} + +PyObject *pkcs5_pbkdf2_hmac_sha1(PyObject *pass, + PyObject *salt, + int iter, + int keylen) { + unsigned char key[EVP_MAX_KEY_LENGTH]; + unsigned char *saltbuf; + char *passbuf; + PyObject *ret; + int passlen, saltlen; + + if (m2_PyObject_AsReadBufferInt(pass, (const void **)&passbuf, + &passlen) == -1) + return NULL; + if (m2_PyObject_AsReadBufferInt(salt, (const void **)&saltbuf, + &saltlen) == -1) + return NULL; + + PKCS5_PBKDF2_HMAC_SHA1(passbuf, passlen, saltbuf, saltlen, iter, + keylen, key); + ret = PyString_FromStringAndSize((char*)key, keylen); + OPENSSL_cleanse(key, keylen); + return ret; +} + +EVP_MD_CTX *md_ctx_new(void) { + EVP_MD_CTX *ctx; + + if (!(ctx = EVP_MD_CTX_create())) { + PyErr_SetString(PyExc_MemoryError, "md_ctx_new"); + } + return ctx; +} + +void md_ctx_free(EVP_MD_CTX *ctx) { + EVP_MD_CTX_destroy(ctx); +} + +int digest_update(EVP_MD_CTX *ctx, PyObject *blob) { + const void *buf; + Py_ssize_t len; + + if (PyObject_AsReadBuffer(blob, &buf, &len) == -1) + return -1; + + return EVP_DigestUpdate(ctx, buf, len); +} + +PyObject *digest_final(EVP_MD_CTX *ctx) { + void *blob; + int blen; + PyObject *ret; + + if (!(blob = PyMem_Malloc(ctx->digest->md_size))) { + PyErr_SetString(PyExc_MemoryError, "digest_final"); + return NULL; + } + if (!EVP_DigestFinal(ctx, blob, (unsigned int *)&blen)) { + PyMem_Free(blob); + PyErr_SetString(_evp_err, ERR_reason_error_string(ERR_get_error())); + return NULL; + } + ret = PyString_FromStringAndSize(blob, blen); + PyMem_Free(blob); + return ret; +} + +HMAC_CTX *hmac_ctx_new(void) { + HMAC_CTX *ctx; + + if (!(ctx = (HMAC_CTX *)PyMem_Malloc(sizeof(HMAC_CTX)))) { + PyErr_SetString(PyExc_MemoryError, "hmac_ctx_new"); + return NULL; + } + HMAC_CTX_init(ctx); + return ctx; +} + +void hmac_ctx_free(HMAC_CTX *ctx) { + HMAC_CTX_cleanup(ctx); + PyMem_Free((void *)ctx); +} + +PyObject *hmac_init(HMAC_CTX *ctx, PyObject *key, const EVP_MD *md) { + const void *kbuf; + int klen; + + if (m2_PyObject_AsReadBufferInt(key, &kbuf, &klen) == -1) + return NULL; + + HMAC_Init(ctx, kbuf, klen, md); + Py_INCREF(Py_None); + return Py_None; +} + +PyObject *hmac_update(HMAC_CTX *ctx, PyObject *blob) { + const void *buf; + Py_ssize_t len; + + if (PyObject_AsReadBuffer(blob, &buf, &len) == -1) + return NULL; + + HMAC_Update(ctx, buf, len); + Py_INCREF(Py_None); + return Py_None; +} + +PyObject *hmac_final(HMAC_CTX *ctx) { + void *blob; + int blen; + PyObject *ret; + + if (!(blob = PyMem_Malloc(ctx->md->md_size))) { + PyErr_SetString(PyExc_MemoryError, "hmac_final"); + return NULL; + } + HMAC_Final(ctx, blob, (unsigned int *)&blen); + ret = PyString_FromStringAndSize(blob, blen); + PyMem_Free(blob); + return ret; +} + +PyObject *hmac(PyObject *key, PyObject *data, const EVP_MD *md) { + const void *kbuf, *dbuf; + void *blob; + int klen; + unsigned int blen; + Py_ssize_t dlen; + PyObject *ret; + + if ((m2_PyObject_AsReadBufferInt(key, &kbuf, &klen) == -1) + || (PyObject_AsReadBuffer(data, &dbuf, &dlen) == -1)) + return NULL; + + if (!(blob = PyMem_Malloc(EVP_MAX_MD_SIZE))) { + PyErr_SetString(PyExc_MemoryError, "hmac"); + return NULL; + } + HMAC(md, kbuf, klen, dbuf, dlen, blob, &blen); + blob = PyMem_Realloc(blob, blen); + ret = PyString_FromStringAndSize(blob, blen); + PyMem_Free(blob); + return ret; +} + +EVP_CIPHER_CTX *cipher_ctx_new(void) { + EVP_CIPHER_CTX *ctx; + + if (!(ctx = (EVP_CIPHER_CTX *)PyMem_Malloc(sizeof(EVP_CIPHER_CTX)))) { + PyErr_SetString(PyExc_MemoryError, "cipher_ctx_new"); + return NULL; + } + EVP_CIPHER_CTX_init(ctx); + return ctx; +} + +void cipher_ctx_free(EVP_CIPHER_CTX *ctx) { + EVP_CIPHER_CTX_cleanup(ctx); + PyMem_Free((void *)ctx); +} + +PyObject *bytes_to_key(const EVP_CIPHER *cipher, EVP_MD *md, + PyObject *data, PyObject *salt, + PyObject *iv, /* Not used */ + int iter) { + unsigned char key[EVP_MAX_KEY_LENGTH]; + const void *dbuf, *sbuf; + int dlen, klen; + Py_ssize_t slen; + PyObject *ret; + + if ((m2_PyObject_AsReadBufferInt(data, &dbuf, &dlen) == -1) + || (PyObject_AsReadBuffer(salt, &sbuf, &slen) == -1)) + return NULL; + + assert((slen == 8) || (slen == 0)); + klen = EVP_BytesToKey(cipher, md, (unsigned char *)sbuf, + (unsigned char *)dbuf, dlen, iter, + key, NULL); /* Since we are not returning IV no need to derive it */ + ret = PyString_FromStringAndSize((char*)key, klen); + return ret; +} + +PyObject *cipher_init(EVP_CIPHER_CTX *ctx, const EVP_CIPHER *cipher, + PyObject *key, PyObject *iv, int mode) { + const void *kbuf, *ibuf; + Py_ssize_t klen, ilen; + + if ((PyObject_AsReadBuffer(key, &kbuf, &klen) == -1) + || (PyObject_AsReadBuffer(iv, &ibuf, &ilen) == -1)) + return NULL; + + if (!EVP_CipherInit(ctx, cipher, (unsigned char *)kbuf, + (unsigned char *)ibuf, mode)) { + PyErr_SetString(_evp_err, ERR_reason_error_string(ERR_get_error())); + return NULL; + } + Py_INCREF(Py_None); + return Py_None; +} + +PyObject *cipher_update(EVP_CIPHER_CTX *ctx, PyObject *blob) { + const void *buf; + int len, olen; + void *obuf; + PyObject *ret; + + if (m2_PyObject_AsReadBufferInt(blob, &buf, &len) == -1) + return NULL; + + if (!(obuf = PyMem_Malloc(len + EVP_CIPHER_CTX_block_size(ctx) - 1))) { + PyErr_SetString(PyExc_MemoryError, "cipher_update"); + return NULL; + } + if (!EVP_CipherUpdate(ctx, obuf, &olen, (unsigned char *)buf, len)) { + PyMem_Free(obuf); + PyErr_SetString(_evp_err, ERR_reason_error_string(ERR_get_error())); + return NULL; + } + ret = PyString_FromStringAndSize(obuf, olen); + PyMem_Free(obuf); + return ret; +} + +PyObject *cipher_final(EVP_CIPHER_CTX *ctx) { + void *obuf; + int olen; + PyObject *ret; + + if (!(obuf = PyMem_Malloc(ctx->cipher->block_size))) { + PyErr_SetString(PyExc_MemoryError, "cipher_final"); + return NULL; + } + if (!EVP_CipherFinal(ctx, (unsigned char *)obuf, &olen)) { + PyMem_Free(obuf); + PyErr_SetString(_evp_err, ERR_reason_error_string(ERR_get_error())); + return NULL; + } + ret = PyString_FromStringAndSize(obuf, olen); + PyMem_Free(obuf); + return ret; +} + +PyObject *sign_update(EVP_MD_CTX *ctx, PyObject *blob) { + const void *buf; + Py_ssize_t len; + + if (PyObject_AsReadBuffer(blob, &buf, &len) == -1) + return NULL; + + if (!EVP_SignUpdate(ctx, buf, len)) { + PyErr_SetString(_evp_err, ERR_reason_error_string(ERR_get_error())); + return NULL; + } + Py_INCREF(Py_None); + return Py_None; +} + +PyObject *sign_final(EVP_MD_CTX *ctx, EVP_PKEY *pkey) { + PyObject *ret; + unsigned char *sigbuf; + unsigned int siglen = EVP_PKEY_size(pkey); + + sigbuf = (unsigned char*)OPENSSL_malloc(siglen); + if (!sigbuf) { + PyErr_SetString(PyExc_MemoryError, "sign_final"); + return NULL; + } + + if (!EVP_SignFinal(ctx, sigbuf, &siglen, pkey)) { + OPENSSL_cleanse(sigbuf, siglen); + OPENSSL_free(sigbuf); + PyErr_SetString(_evp_err, ERR_reason_error_string(ERR_get_error())); + return NULL; + } + ret = PyString_FromStringAndSize((char*)sigbuf, siglen); + OPENSSL_cleanse(sigbuf, siglen); + OPENSSL_free(sigbuf); + return ret; +} + +int verify_update(EVP_MD_CTX *ctx, PyObject *blob) { + const void *buf; + Py_ssize_t len; + + if (PyObject_AsReadBuffer(blob, &buf, &len) == -1) + return -1; + + return EVP_VerifyUpdate(ctx, buf, len); +} + + +int verify_final(EVP_MD_CTX *ctx, PyObject *blob, EVP_PKEY *pkey) { + unsigned char *kbuf; + int len; + + if (m2_PyObject_AsReadBufferInt(blob, (const void **)&kbuf, &len) == -1) + return -1; + + return EVP_VerifyFinal(ctx, kbuf, len, pkey); +} +%} + +%inline %{ +int pkey_write_pem_no_cipher(EVP_PKEY *pkey, BIO *f, PyObject *pyfunc) { + int ret; + + Py_INCREF(pyfunc); + Py_BEGIN_ALLOW_THREADS + ret = PEM_write_bio_PKCS8PrivateKey(f, pkey, NULL, NULL, 0, + passphrase_callback, (void *)pyfunc); + Py_END_ALLOW_THREADS + Py_DECREF(pyfunc); + return ret; +} +%} + +%inline %{ +int pkey_write_pem(EVP_PKEY *pkey, BIO *f, EVP_CIPHER *cipher, PyObject *pyfunc) { + int ret; + + Py_INCREF(pyfunc); + Py_BEGIN_ALLOW_THREADS + ret = PEM_write_bio_PKCS8PrivateKey(f, pkey, cipher, NULL, 0, + passphrase_callback, (void *)pyfunc); + Py_END_ALLOW_THREADS + Py_DECREF(pyfunc); + return ret; +} +%} + +%inline %{ +EVP_PKEY *pkey_read_pem(BIO *f, PyObject *pyfunc) { + EVP_PKEY *pk; + + Py_INCREF(pyfunc); + Py_BEGIN_ALLOW_THREADS + pk = PEM_read_bio_PrivateKey(f, NULL, passphrase_callback, (void *)pyfunc); + Py_END_ALLOW_THREADS + Py_DECREF(pyfunc); + return pk; +} + +int pkey_assign_rsa(EVP_PKEY *pkey, RSA *rsa) { + return EVP_PKEY_assign_RSA(pkey, rsa); +} + +PyObject *pkey_as_der(EVP_PKEY *pkey) { + unsigned char * pp = NULL; + int len; + PyObject * der; + len = i2d_PUBKEY(pkey, &pp); + if (len < 0){ + PyErr_SetString(PyExc_ValueError, "EVP_PKEY as DER failed"); + return NULL; + } + der = PyString_FromStringAndSize((char*)pp, len); + OPENSSL_free(pp); + return der; +} + +PyObject *pkey_get_modulus(EVP_PKEY *pkey) +{ + RSA *rsa; + DSA *dsa; + BIO *bio; + BUF_MEM *bptr; + PyObject *ret; + + switch (pkey->type) { + case EVP_PKEY_RSA: + rsa = EVP_PKEY_get1_RSA(pkey); + + bio = BIO_new(BIO_s_mem()); + if (!bio) { + RSA_free(rsa); + PyErr_SetString(PyExc_MemoryError, "pkey_get_modulus"); + return NULL; + } + + if (!BN_print(bio, rsa->n)) { + PyErr_SetString(PyExc_RuntimeError, + ERR_error_string(ERR_get_error(), NULL)); + BIO_free(bio); + RSA_free(rsa); + return NULL; + } + BIO_get_mem_ptr(bio, &bptr); + ret = PyString_FromStringAndSize(bptr->data, bptr->length); + BIO_set_close(bio, BIO_CLOSE); + BIO_free(bio); + RSA_free(rsa); + + break; + + case EVP_PKEY_DSA: + dsa = EVP_PKEY_get1_DSA(pkey); + + bio = BIO_new(BIO_s_mem()); + if (!bio) { + DSA_free(dsa); + PyErr_SetString(PyExc_MemoryError, "pkey_get_modulus"); + return NULL; + } + + if (!BN_print(bio, dsa->pub_key)) { + PyErr_SetString(PyExc_RuntimeError, + ERR_error_string(ERR_get_error(), NULL)); + BIO_free(bio); + DSA_free(dsa); + return NULL; + } + BIO_get_mem_ptr(bio, &bptr); + ret = PyString_FromStringAndSize(bptr->data, bptr->length); + BIO_set_close(bio, BIO_CLOSE); + BIO_free(bio); + DSA_free(dsa); + + break; + + default: + PyErr_SetString(PyExc_ValueError, "unsupported key type"); + return NULL; + } + + return ret; +} + + +%} + diff --git a/SWIG/_lib.h b/SWIG/_lib.h new file mode 100644 index 0000000..b53bc2a --- /dev/null +++ b/SWIG/_lib.h @@ -0,0 +1,27 @@ +/* Copyright (c) 1999 Ng Pheng Siong. All rights reserved. */ +/* $Id: _lib.h 593 2007-10-12 21:46:34Z heikki $ */ + +#if PY_VERSION_HEX < 0x02050000 && !defined(PY_SSIZE_T_MIN) +typedef int Py_ssize_t; +#define PY_SSIZE_T_MAX INT_MAX +#define PY_SSIZE_T_MIN INT_MIN +#endif + +typedef struct _blob { + unsigned char *data; + int len; +} Blob; + +Blob *blob_new(int len, const char *errmsg); +Blob *blob_copy(Blob *from, const char *errmsg); +void blob_free(Blob *blob); + +static int m2_PyObject_AsReadBufferInt(PyObject *obj, const void **buffer, + int *buffer_len); +static int m2_PyString_AsStringAndSizeInt(PyObject *obj, char **s, int *len); + +void gen_callback(int p, int n, void *arg); +int passphrase_callback(char *buf, int num, int v, void *userdata); + +void lib_init(void); + diff --git a/SWIG/_lib.i b/SWIG/_lib.i new file mode 100644 index 0000000..42dc180 --- /dev/null +++ b/SWIG/_lib.i @@ -0,0 +1,511 @@ +/* Copyright (c) 1999-2004 Ng Pheng Siong. All rights reserved. */ +/* $Id: _lib.i 695 2009-07-24 06:37:01Z heikki $ */ + +%{ +#include <openssl/dh.h> +#include <openssl/err.h> +#include <openssl/evp.h> +#include <openssl/rsa.h> +#include <openssl/ssl.h> +#include <openssl/x509.h> +#include <ceval.h> + +/* Blob interface. Deprecated. */ + +Blob *blob_new(int len, const char *errmsg) { + + Blob *blob; + if (!(blob=(Blob *)PyMem_Malloc(sizeof(Blob)))){ + PyErr_SetString(PyExc_MemoryError, errmsg); + return NULL; + } + if (!(blob->data=(unsigned char *)PyMem_Malloc(len))) { + PyMem_Free(blob); + PyErr_SetString(PyExc_MemoryError, errmsg); + return NULL; + } + blob->len=len; + return blob; +} + +Blob *blob_copy(Blob *from, const char *errmsg) { + Blob *blob=blob_new(from->len, errmsg); + if (!blob) { + PyErr_SetString(PyExc_MemoryError, errmsg); + return NULL; + } + memcpy(blob->data, from->data, from->len); + return blob; +} + +void blob_free(Blob *blob) { + PyMem_Free(blob->data); + PyMem_Free(blob); +} + + +/* Python helpers. */ + +%} +%ignore m2_PyObject_AsReadBufferInt; +%ignore m2_PyString_AsStringAndSizeInt; +%{ +static int +m2_PyObject_AsReadBufferInt(PyObject *obj, const void **buffer, + int *buffer_len) +{ + int ret; + Py_ssize_t len; + + ret = PyObject_AsReadBuffer(obj, buffer, &len); + if (ret) + return ret; + if (len > INT_MAX) { + PyErr_SetString(PyExc_ValueError, "object too large"); + return -1; + } + *buffer_len = len; + return 0; +} + +static int +m2_PyString_AsStringAndSizeInt(PyObject *obj, char **s, int *len) +{ + int ret; + Py_ssize_t len2; + + ret = PyString_AsStringAndSize(obj, s, &len2); + if (ret) + return ret; + if (len2 > INT_MAX) { + PyErr_SetString(PyExc_ValueError, "string too large"); + return -1; + } + *len = len2; + return 0; +} + + +/* C callbacks invoked by OpenSSL; these in turn call back into +Python. */ + +int ssl_verify_callback(int ok, X509_STORE_CTX *ctx) { + PyObject *argv, *ret; + PyObject *_x509_store_ctx_swigptr=0, *_x509_store_ctx_obj=0, *_x509_store_ctx_inst=0, *_klass=0; + PyObject *_x509=0, *_ssl_ctx=0; + SSL *ssl; + SSL_CTX *ssl_ctx; + X509 *x509; + int errnum, errdepth; + int cret; + int new_style_callback = 0, warning_raised_exception=0; + PyGILState_STATE gilstate; + + ssl = (SSL *)X509_STORE_CTX_get_app_data(ctx); + + gilstate = PyGILState_Ensure(); + + if (PyMethod_Check(ssl_verify_cb_func)) { + PyObject *func; + PyCodeObject *code; + func = PyMethod_Function(ssl_verify_cb_func); + code = (PyCodeObject *) PyFunction_GetCode(func); + if (code && code->co_argcount == 3) { /* XXX Python internals */ + new_style_callback = 1; + } + } else if (PyFunction_Check(ssl_verify_cb_func)) { + PyCodeObject *code = (PyCodeObject *) PyFunction_GetCode(ssl_verify_cb_func); + if (code && code->co_argcount == 2) { /* XXX Python internals */ + new_style_callback = 1; + } + } else { + /* XXX There are lots of other callable types, but we will assume + * XXX that any other type of callable uses the new style callback, + * XXX although this is not entirely safe assumption. + */ + new_style_callback = 1; + } + + if (new_style_callback) { + PyObject *x509mod = PyDict_GetItemString(PyImport_GetModuleDict(), "M2Crypto.X509"); + _klass = PyObject_GetAttrString(x509mod, "X509_Store_Context"); + + _x509_store_ctx_swigptr = SWIG_NewPointerObj((void *)ctx, SWIGTYPE_p_X509_STORE_CTX, 0); + _x509_store_ctx_obj = Py_BuildValue("(Oi)", _x509_store_ctx_swigptr, 0); + _x509_store_ctx_inst = PyInstance_New(_klass, _x509_store_ctx_obj, NULL); + argv = Py_BuildValue("(iO)", ok, _x509_store_ctx_inst); + } else { + if (PyErr_Warn(PyExc_DeprecationWarning, "Old style callback, use cb_func(ok, store) instead")) { + warning_raised_exception = 1; + } + + x509 = X509_STORE_CTX_get_current_cert(ctx); + errnum = X509_STORE_CTX_get_error(ctx); + errdepth = X509_STORE_CTX_get_error_depth(ctx); + + ssl = (SSL *)X509_STORE_CTX_get_app_data(ctx); + ssl_ctx = SSL_get_SSL_CTX(ssl); + + _x509 = SWIG_NewPointerObj((void *)x509, SWIGTYPE_p_X509, 0); + _ssl_ctx = SWIG_NewPointerObj((void *)ssl_ctx, SWIGTYPE_p_SSL_CTX, 0); + argv = Py_BuildValue("(OOiii)", _ssl_ctx, _x509, errnum, errdepth, ok); + } + + if (!warning_raised_exception) { + ret = PyEval_CallObject(ssl_verify_cb_func, argv); + } else { + ret = 0; + } + + if (!ret) { + /* Got an exception in PyEval_CallObject(), let's fail verification + * to be safe. + */ + cret = 0; + } else { + cret = (int)PyInt_AsLong(ret); + } + Py_XDECREF(ret); + Py_XDECREF(argv); + if (new_style_callback) { + Py_XDECREF(_x509_store_ctx_inst); + Py_XDECREF(_x509_store_ctx_obj); + Py_XDECREF(_x509_store_ctx_swigptr); + Py_XDECREF(_klass); + } else { + Py_XDECREF(_x509); + Py_XDECREF(_ssl_ctx); + } + + PyGILState_Release(gilstate); + + return cret; +} + +void ssl_info_callback(const SSL *s, int where, int ret) { + PyObject *argv, *retval, *_SSL; + PyGILState_STATE gilstate; + + gilstate = PyGILState_Ensure(); + + _SSL = SWIG_NewPointerObj((void *)s, SWIGTYPE_p_SSL, 0); + argv = Py_BuildValue("(iiO)", where, ret, _SSL); + + retval = PyEval_CallObject(ssl_info_cb_func, argv); + + Py_XDECREF(retval); + Py_XDECREF(argv); + Py_XDECREF(_SSL); + + PyGILState_Release(gilstate); +} + +DH *ssl_set_tmp_dh_callback(SSL *ssl, int is_export, int keylength) { + PyObject *argv, *ret, *_ssl; + DH *dh; + PyGILState_STATE gilstate; + + gilstate = PyGILState_Ensure(); + + _ssl = SWIG_NewPointerObj((void *)ssl, SWIGTYPE_p_SSL, 0); + argv = Py_BuildValue("(Oii)", _ssl, is_export, keylength); + + ret = PyEval_CallObject(ssl_set_tmp_dh_cb_func, argv); + + if ((SWIG_ConvertPtr(ret, (void **)&dh, SWIGTYPE_p_DH, SWIG_POINTER_EXCEPTION | 0)) == -1) + dh = NULL; + Py_XDECREF(ret); + Py_XDECREF(argv); + Py_XDECREF(_ssl); + + PyGILState_Release(gilstate); + + return dh; +} + +RSA *ssl_set_tmp_rsa_callback(SSL *ssl, int is_export, int keylength) { + PyObject *argv, *ret, *_ssl; + RSA *rsa; + PyGILState_STATE gilstate; + + gilstate = PyGILState_Ensure(); + + _ssl = SWIG_NewPointerObj((void *)ssl, SWIGTYPE_p_SSL, 0); + argv = Py_BuildValue("(Oii)", _ssl, is_export, keylength); + + ret = PyEval_CallObject(ssl_set_tmp_rsa_cb_func, argv); + + if ((SWIG_ConvertPtr(ret, (void **)&rsa, SWIGTYPE_p_RSA, SWIG_POINTER_EXCEPTION | 0)) == -1) + rsa = NULL; + Py_XDECREF(ret); + Py_XDECREF(argv); + Py_XDECREF(_ssl); + + PyGILState_Release(gilstate); + + return rsa; +} + +void gen_callback(int p, int n, void *arg) { + PyObject *argv, *ret, *cbfunc; + + PyGILState_STATE gilstate; + gilstate = PyGILState_Ensure(); + cbfunc = (PyObject *)arg; + argv = Py_BuildValue("(ii)", p, n); + ret = PyEval_CallObject(cbfunc, argv); + Py_DECREF(argv); + Py_XDECREF(ret); + PyGILState_Release(gilstate); +} + +int passphrase_callback(char *buf, int num, int v, void *arg) { + int i; + Py_ssize_t len; + char *str; + PyObject *argv, *ret, *cbfunc; + PyGILState_STATE gilstate; + + gilstate = PyGILState_Ensure(); + cbfunc = (PyObject *)arg; + argv = Py_BuildValue("(i)", v); + ret = PyEval_CallObject(cbfunc, argv); + Py_DECREF(argv); + if (ret == NULL) { + PyGILState_Release(gilstate); + return -1; + } + if (!PyString_Check(ret)) { + Py_DECREF(ret); + PyGILState_Release(gilstate); + return -1; + } + if ((len = PyString_Size(ret)) > num) + len = num; + str = PyString_AsString(ret); + for (i = 0; i < len; i++) + buf[i] = str[i]; + Py_DECREF(ret); + PyGILState_Release(gilstate); + return len; +} +%} + +%inline %{ +void lib_init() { + SSLeay_add_all_algorithms(); + ERR_load_ERR_strings(); +} + +/* Bignum routines that aren't not numerous enough to +warrant a separate file. */ + +PyObject *bn_to_mpi(BIGNUM *bn) { + int len; + unsigned char *mpi; + PyObject *pyo; + + len = BN_bn2mpi(bn, NULL); + if (!(mpi=(unsigned char *)PyMem_Malloc(len))) { + PyErr_SetString(PyExc_RuntimeError, + ERR_error_string(ERR_get_error(), NULL)); + return NULL; + } + len=BN_bn2mpi(bn, mpi); + pyo=PyString_FromStringAndSize((const char *)mpi, len); + PyMem_Free(mpi); + return pyo; +} + +BIGNUM *mpi_to_bn(PyObject *value) { + const void *vbuf; + int vlen; + + if (m2_PyObject_AsReadBufferInt(value, &vbuf, &vlen) == -1) + return NULL; + + return BN_mpi2bn(vbuf, vlen, NULL); +} + +PyObject *bn_to_bin(BIGNUM *bn) { + int len; + unsigned char *bin; + PyObject *pyo; + + len = BN_num_bytes(bn); + if (!(bin=(unsigned char *)PyMem_Malloc(len))) { + PyErr_SetString(PyExc_MemoryError, "bn_to_bin"); + return NULL; + } + BN_bn2bin(bn, bin); + pyo=PyString_FromStringAndSize((const char *)bin, len); + PyMem_Free(bin); + return pyo; +} + +BIGNUM *bin_to_bn(PyObject *value) { + const void *vbuf; + int vlen; + + if (m2_PyObject_AsReadBufferInt(value, &vbuf, &vlen) == -1) + return NULL; + + return BN_bin2bn(vbuf, vlen, NULL); +} + +PyObject *bn_to_hex(BIGNUM *bn) { + char *hex; + PyObject *pyo; + Py_ssize_t len; + + hex = BN_bn2hex(bn); + if (!hex) { + PyErr_SetString(PyExc_RuntimeError, + ERR_error_string(ERR_get_error(), NULL)); + OPENSSL_free(hex); + return NULL; + } + len = strlen(hex); + pyo=PyString_FromStringAndSize(hex, len); + OPENSSL_free(hex); + return pyo; +} + +BIGNUM *hex_to_bn(PyObject *value) { + const void *vbuf; + Py_ssize_t vlen; + BIGNUM *bn; + + if (PyObject_AsReadBuffer(value, &vbuf, &vlen) == -1) + return NULL; + + if ((bn=BN_new())==NULL) { + PyErr_SetString(PyExc_MemoryError, "hex_to_bn"); + return NULL; + } + if (BN_hex2bn(&bn, (const char *)vbuf) <= 0) { + PyErr_SetString(PyExc_RuntimeError, + ERR_error_string(ERR_get_error(), NULL)); + BN_free(bn); + return NULL; + } + return bn; +} + +BIGNUM *dec_to_bn(PyObject *value) { + const void *vbuf; + Py_ssize_t vlen; + BIGNUM *bn; + + if (PyObject_AsReadBuffer(value, &vbuf, &vlen) == -1) + return NULL; + + if ((bn=BN_new())==NULL) { + PyErr_SetString(PyExc_MemoryError, "dec_to_bn"); + return NULL; + } + if ((BN_dec2bn(&bn, (const char *)vbuf) <= 0)) { + PyErr_SetString(PyExc_RuntimeError, + ERR_error_string(ERR_get_error(), NULL)); + BN_free(bn); + return NULL; + } + return bn; +} +%} + + +/* Various useful typemaps. */ + +%typemap(in) Blob * { + Py_ssize_t len; + + if (!PyString_Check($input)) { + PyErr_SetString(PyExc_TypeError, "expected PyString"); + return NULL; + } + len=PyString_Size($input); + if (len > INT_MAX) { + PyErr_SetString(PyExc_ValueError, "object too large"); + return -1; + } + $1=(Blob *)PyMem_Malloc(sizeof(Blob)); + if (!$1) { + PyErr_SetString(PyExc_MemoryError, "malloc Blob"); + return NULL; + } + $1->data=(unsigned char *)PyString_AsString($input); + $1->len=len; +} + +%typemap(out) Blob * { + if ($1==NULL) { + Py_INCREF(Py_None); + $result=Py_None; + } else { + $result=PyString_FromStringAndSize((const char *)$1->data, $1->len); + PyMem_Free($1->data); + PyMem_Free($1); + } +} + +%typemap(in) FILE * { + if (!PyFile_Check($input)) { + PyErr_SetString(PyExc_TypeError, "expected PyFile"); + return NULL; + } + $1=PyFile_AsFile($input); +} + +%typemap(in) PyObject *pyfunc { + if (!PyCallable_Check($input)) { + PyErr_SetString(PyExc_TypeError, "expected PyCallable"); + return NULL; + } + $1=$input; +} + +%typemap(in) PyObject *pyblob { + if (!PyString_Check($input)) { + PyErr_SetString(PyExc_TypeError, "expected PyString"); + return NULL; + } + $1=$input; +} + +%typemap(in) PyObject * { + $1=$input; +} + +%typemap(out) PyObject * { + $result=$1; +} + +%typemap(out) int { + $result=PyInt_FromLong($1); + if (PyErr_Occurred()) SWIG_fail; +} + +/* Pointer checks. */ + +%apply Pointer NONNULL { Blob * }; + + +/* A bunch of "straight-thru" functions. */ + +%rename(err_print_errors_fp) ERR_print_errors_fp; +%threadallow ERR_print_errors_fp; +extern void ERR_print_errors_fp(FILE *); +%rename(err_print_errors) ERR_print_errors; +%threadallow ERR_print_errors; +extern void ERR_print_errors(BIO *); +%rename(err_get_error) ERR_get_error; +extern unsigned long ERR_get_error(void); +%rename(err_peek_error) ERR_peek_error; +extern unsigned long ERR_peek_error(void); +%rename(err_lib_error_string) ERR_lib_error_string; +extern const char *ERR_lib_error_string(unsigned long); +%rename(err_func_error_string) ERR_func_error_string; +extern const char *ERR_func_error_string(unsigned long); +%rename(err_reason_error_string) ERR_reason_error_string; +extern const char *ERR_reason_error_string(unsigned long); diff --git a/SWIG/_m2crypto.def b/SWIG/_m2crypto.def new file mode 100644 index 0000000..753db2c --- /dev/null +++ b/SWIG/_m2crypto.def @@ -0,0 +1,2 @@ +EXPORTS
+init__m2crypto diff --git a/SWIG/_m2crypto.i b/SWIG/_m2crypto.i new file mode 100644 index 0000000..3d779a1 --- /dev/null +++ b/SWIG/_m2crypto.i @@ -0,0 +1,82 @@ +/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* Copyright (c) 1999-2002 Ng Pheng Siong. All rights reserved. + * + * Portions created by Open Source Applications Foundation (OSAF) are + * Copyright (C) 2004-2006 OSAF. All Rights Reserved. + * + * Copyright (c) 2009-2010 Heikki Toivonen. All rights reserved. + * + */ + +%module(threads=1) _m2crypto +/* We really don't need threadblock (PyGILState_Ensure() etc.) anywhere. + Disable threadallow as well, only enable it for operations likely to + block. */ +%nothreadblock; +%nothreadallow; + +%{ +#include <openssl/err.h> +#include <openssl/rand.h> +#include <_lib.h> + +#include "compile.h" + +static PyObject *ssl_verify_cb_func; +static PyObject *ssl_info_cb_func; +static PyObject *ssl_set_tmp_dh_cb_func; +static PyObject *ssl_set_tmp_rsa_cb_func; +%} + +%include <openssl/opensslv.h> +#if OPENSSL_VERSION_NUMBER >= 0x0090707fL +#define CONST const +#else +#define CONST +#endif + +#if OPENSSL_VERSION_NUMBER >= 0x0090800fL +#define CONST098 const +#else +#define CONST098 +#endif + +/* Bring in STACK_OF macro definition */ +%include <openssl/safestack.h> + +/* Bring in LHASH_OF macro definition */ +/* XXX Can't include lhash.h where LHASH_OF is defined, because it includes + XXX stdio.h etc. which we fail to include. So we have to (re)define + XXX LHASH_OF here instead. +%include <openssl/lhash.h> +*/ +#if OPENSSL_VERSION_NUMBER >= 0x10000000L +#define LHASH_OF(type) struct lhash_st_##type +#endif + +%include constraints.i +%include _threads.i +%include _lib.i +%include _bio.i +%include _bn.i +%include _rand.i +%include _evp.i +%include _aes.i +%include _rc4.i +%include _dh.i +%include _rsa.i +%include _dsa.i +%include _ssl.i +%include _x509.i +%include _asn1.i +%include _pkcs7.i +%include _util.i +%include _ec.i +%include _engine.i +%include _objects.i + +#ifdef SWIG_VERSION +%constant int encrypt = 1; +%constant int decrypt = 0; +#endif + diff --git a/SWIG/_objects.i b/SWIG/_objects.i new file mode 100644 index 0000000..40f5e51 --- /dev/null +++ b/SWIG/_objects.i @@ -0,0 +1,95 @@ +/* + * -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * vim: syntax=c sts=4 sw=4 + * + * ASN1_OBJECT manipulation functions from OBJ_obj2txt(3SSL). + * + * Pavel Shramov + * IMEC MSU + */ +%{ +#include <openssl/objects.h> +%} + +%apply Pointer NONNULL { ASN1_OBJECT * }; +%apply Pointer NONNULL { const char * }; + +%rename(obj_nid2obj) OBJ_nid2obj; +extern ASN1_OBJECT * OBJ_nid2obj(int n); +%rename(obj_nid2ln) OBJ_nid2ln; +extern const char * OBJ_nid2ln(int n); +%rename(obj_nid2sn) OBJ_nid2sn; +extern const char * OBJ_nid2sn(int n); + +%rename(obj_obj2nid) OBJ_obj2nid; +extern int OBJ_obj2nid(const ASN1_OBJECT *o); + +%rename(obj_ln2nid) OBJ_ln2nid; +extern int OBJ_ln2nid(const char *ln); +%rename(obj_sn2nid) OBJ_sn2nid; +extern int OBJ_sn2nid(const char *sn); + +%rename(obj_txt2nid) OBJ_txt2nid; +extern int OBJ_txt2nid(const char *s); + +%rename(obj_txt2obj) OBJ_txt2obj; +extern ASN1_OBJECT * OBJ_txt2obj(const char *s, int no_name); + + +%rename(_obj_obj2txt) OBJ_obj2txt; +extern int OBJ_obj2txt(char *, int, const ASN1_OBJECT *, int); + + +%inline %{ +/* + From the manpage for OBJ_obt2txt (): + BUGS + OBJ_obj2txt() is awkward and messy to use: it doesn’t follow the + convention of other OpenSSL functions where the buffer can be set + to NULL to determine the amount of data that should be written. + Instead buf must point to a valid buffer and buf_len should be set + to a positive value. A buffer length of 80 should be more than + enough to handle any OID encountered in practice. + + The first call to OBJ_obj2txt () therefore passes a non-NULL dummy + buffer. This wart is reportedly removed in OpenSSL 0.9.8b, although + the manpage has not been updated. + + OBJ_obj2txt always prints \0 at the end. But the return value + is the number of "good" bytes written. So memory is allocated for + len + 1 bytes but only len bytes are marshalled to python. +*/ +PyObject *obj_obj2txt(const ASN1_OBJECT *obj, int no_name) +{ + int len; + PyObject *ret; + char *buf; + char dummy[1]; + + len = OBJ_obj2txt(dummy, 1, obj, no_name); + if (len < 0) { + PyErr_SetString(PyExc_RuntimeError, ERR_reason_error_string(ERR_get_error())); + return NULL; + } else if (len == 0) { + /* XXX: For OpenSSL prior to 0.9.8b. + + Changes between 0.9.8a and 0.9.8b [04 May 2006] + ... + *) Several fixes and enhancements to the OID generation code. The old code + sometimes allowed invalid OIDs (1.X for X >= 40 for example), couldn't + handle numbers larger than ULONG_MAX, truncated printing and had a + non standard OBJ_obj2txt() behaviour. + [Steve Henson] + */ + + len = 80; + } + + buf = PyMem_Malloc(len + 1); + len = OBJ_obj2txt(buf, len + 1, obj, no_name); + ret = PyString_FromStringAndSize(buf, len); + PyMem_Free(buf); + + return ret; +} +%} diff --git a/SWIG/_pkcs7.i b/SWIG/_pkcs7.i new file mode 100644 index 0000000..174f40a --- /dev/null +++ b/SWIG/_pkcs7.i @@ -0,0 +1,241 @@ +/* Copyright (c) 2000 Ng Pheng Siong. All rights reserved. + * Copyright (c) 2009-2010 Heikki Toivonen. All rights reserved. +*/ +/* $Id: _pkcs7.i 723 2010-02-13 06:53:13Z heikki $ */ + +%{ +#include <openssl/bio.h> +#include <openssl/evp.h> +#include <openssl/objects.h> +#include <openssl/pkcs7.h> +%} + +%apply Pointer NONNULL { BIO * }; +%apply Pointer NONNULL { EVP_CIPHER * }; +%apply Pointer NONNULL { EVP_PKEY * }; +%apply Pointer NONNULL { PKCS7 * }; +%apply Pointer NONNULL { STACK_OF(X509) * }; +%apply Pointer NONNULL { X509 * }; + +%rename(pkcs7_new) PKCS7_new; +extern PKCS7 *PKCS7_new(void); +%rename(pkcs7_free) PKCS7_free; +extern void PKCS7_free(PKCS7 *); +%rename(pkcs7_add_certificate) PKCS7_add_certificate; +extern void PKCS7_add_certificate(PKCS7 *, X509 *); + +/* S/MIME operation */ +%constant int PKCS7_TEXT = 0x1; +%constant int PKCS7_NOCERTS = 0x2; +%constant int PKCS7_NOSIGS = 0x4; +%constant int PKCS7_NOCHAIN = 0x8; +%constant int PKCS7_NOINTERN = 0x10; +%constant int PKCS7_NOVERIFY = 0x20; +%constant int PKCS7_DETACHED = 0x40; +%constant int PKCS7_BINARY = 0x80; +%constant int PKCS7_NOATTR = 0x100; + +%constant int PKCS7_SIGNED = NID_pkcs7_signed; +%constant int PKCS7_ENVELOPED = NID_pkcs7_enveloped; +%constant int PKCS7_SIGNED_ENVELOPED = NID_pkcs7_signedAndEnveloped; +%constant int PKCS7_DATA = NID_pkcs7_data; + +%inline %{ +static PyObject *_pkcs7_err, *_smime_err; + +void pkcs7_init(PyObject *pkcs7_err) { + Py_INCREF(pkcs7_err); + _pkcs7_err = pkcs7_err; +} + +void smime_init(PyObject *smime_err) { + Py_INCREF(smime_err); + _smime_err = smime_err; +} +%} + +%threadallow pkcs7_encrypt; +%inline %{ +PKCS7 *pkcs7_encrypt(STACK_OF(X509) *stack, BIO *bio, EVP_CIPHER *cipher, int flags) { + return PKCS7_encrypt(stack, bio, cipher, flags); +} + +PyObject *pkcs7_decrypt(PKCS7 *pkcs7, EVP_PKEY *pkey, X509 *cert, int flags) { + int outlen; + char *outbuf; + BIO *bio; + PyObject *ret; + + if (!(bio=BIO_new(BIO_s_mem()))) { + PyErr_SetString(PyExc_MemoryError, "pkcs7_decrypt"); + return NULL; + } + if (!PKCS7_decrypt(pkcs7, pkey, cert, bio, flags)) { + PyErr_SetString(_pkcs7_err, ERR_reason_error_string(ERR_get_error())); + BIO_free(bio); + return NULL; + } + outlen = BIO_ctrl_pending(bio); + if (!(outbuf=(char *)PyMem_Malloc(outlen))) { + PyErr_SetString(PyExc_MemoryError, "pkcs7_decrypt"); + BIO_free(bio); + return NULL; + } + BIO_read(bio, outbuf, outlen); + ret = PyString_FromStringAndSize(outbuf, outlen); + BIO_free(bio); + PyMem_Free(outbuf); + return ret; +} +%} + +%threadallow pkcs7_sign0; +%inline %{ +PKCS7 *pkcs7_sign0(X509 *x509, EVP_PKEY *pkey, BIO *bio, int flags) { + return PKCS7_sign(x509, pkey, NULL, bio, flags); +} +%} + +%threadallow pkcs7_sign1; +%inline %{ +PKCS7 *pkcs7_sign1(X509 *x509, EVP_PKEY *pkey, STACK_OF(X509) *stack, BIO *bio, int flags) { + return PKCS7_sign(x509, pkey, stack, bio, flags); +} +%} + +%inline %{ +PyObject *pkcs7_verify1(PKCS7 *pkcs7, STACK_OF(X509) *stack, X509_STORE *store, BIO *data, int flags) { + int res, outlen; + char *outbuf; + BIO *bio; + PyObject *ret; + + if (!(bio=BIO_new(BIO_s_mem()))) { + PyErr_SetString(PyExc_MemoryError, "pkcs7_verify1"); + return NULL; + } + Py_BEGIN_ALLOW_THREADS + res = PKCS7_verify(pkcs7, stack, store, data, bio, flags); + Py_END_ALLOW_THREADS + if (!res) { + PyErr_SetString(_pkcs7_err, ERR_reason_error_string(ERR_get_error())); + BIO_free(bio); + return NULL; + } + outlen = BIO_ctrl_pending(bio); + if (!(outbuf=(char *)PyMem_Malloc(outlen))) { + PyErr_SetString(PyExc_MemoryError, "pkcs7_verify1"); + BIO_free(bio); + return NULL; + } + BIO_read(bio, outbuf, outlen); + ret = PyString_FromStringAndSize(outbuf, outlen); + BIO_free(bio); + PyMem_Free(outbuf); + return ret; +} + +PyObject *pkcs7_verify0(PKCS7 *pkcs7, STACK_OF(X509) *stack, X509_STORE *store, int flags) { + return pkcs7_verify1(pkcs7, stack, store, NULL, flags); +} +%} + +%threadallow smime_write_pkcs7_multi; +%inline %{ +int smime_write_pkcs7_multi(BIO *bio, PKCS7 *pkcs7, BIO *data, int flags) { + return SMIME_write_PKCS7(bio, pkcs7, data, flags | PKCS7_DETACHED); +} +%} + +%threadallow smime_write_pkcs7; +%inline %{ +int smime_write_pkcs7(BIO *bio, PKCS7 *pkcs7, int flags) { + return SMIME_write_PKCS7(bio, pkcs7, NULL, flags); +} + +PyObject *smime_read_pkcs7(BIO *bio) { + BIO *bcont = NULL; + PKCS7 *p7; + PyObject *tuple, *_p7, *_BIO; + + if (BIO_method_type(bio) == BIO_TYPE_MEM) { + /* OpenSSL FAQ explains that this is needed for mem BIO to return EOF, + * like file BIO does. Might need to do this for more mem BIOs but + * not sure if that is safe, so starting with just this single place. + */ + BIO_set_mem_eof_return(bio, 0); + } + + Py_BEGIN_ALLOW_THREADS + p7=SMIME_read_PKCS7(bio, &bcont); + Py_END_ALLOW_THREADS + if (!p7) { + PyErr_SetString(_smime_err, ERR_reason_error_string(ERR_get_error())); + return NULL; + } + if (!(tuple=PyTuple_New(2))) { + PyErr_SetString(PyExc_RuntimeError, "PyTuple_New() fails"); + return NULL; + } + _p7 = SWIG_NewPointerObj((void *)p7, SWIGTYPE_p_PKCS7, 0); + PyTuple_SET_ITEM(tuple, 0, _p7); + if (!bcont) { + Py_INCREF(Py_None); + PyTuple_SET_ITEM(tuple, 1, Py_None); + } else { + _BIO = SWIG_NewPointerObj((void *)bcont, SWIGTYPE_p_BIO, 0); + PyTuple_SET_ITEM(tuple, 1, _BIO); + } + return tuple; +} +%} + +%threadallow pkcs7_read_bio; +%inline %{ +PKCS7 *pkcs7_read_bio(BIO *bio) { + return PEM_read_bio_PKCS7(bio, NULL, NULL, NULL); +} +%} + +%threadallow pkcs7_read_bio_der; +%inline %{ +PKCS7 *pkcs7_read_bio_der(BIO *bio) { + return d2i_PKCS7_bio(bio, NULL); +} +%} + +%threadallow pkcs7_write_bio; +%inline %{ +int pkcs7_write_bio(PKCS7 *pkcs7, BIO* bio) { + return PEM_write_bio_PKCS7(bio, pkcs7); +} +%} + +%threadallow pkcs7_write_bio_der; +%inline %{ +int pkcs7_write_bio_der(PKCS7 *pkcs7, BIO *bio) { + return i2d_PKCS7_bio(bio, pkcs7); +} + +int pkcs7_type_nid(PKCS7 *pkcs7) { + return OBJ_obj2nid(pkcs7->type); +} + +const char *pkcs7_type_sn(PKCS7 *pkcs7) { + return OBJ_nid2sn(OBJ_obj2nid(pkcs7->type)); +} +%} + +%threadallow smime_crlf_copy; +%inline %{ +int smime_crlf_copy(BIO *in, BIO *out) { + return SMIME_crlf_copy(in, out, PKCS7_TEXT); +} + +/* return STACK_OF(X509)* */ +STACK_OF(X509) *pkcs7_get0_signers(PKCS7 *p7, STACK_OF(X509) *certs, int flags) { + return PKCS7_get0_signers(p7, certs, flags); +} + +%} + diff --git a/SWIG/_rand.i b/SWIG/_rand.i new file mode 100644 index 0000000..2d4f144 --- /dev/null +++ b/SWIG/_rand.i @@ -0,0 +1,122 @@ +/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* Copyright (c) 1999-2004 Ng Pheng Siong. All rights reserved. + * Copyright (c) 2009-2010 Heikki Toivonen. All rights reserved. +*/ +/* $Id: _rand.i 721 2010-02-13 06:30:33Z heikki $ */ + +%module _rand + +%rename(rand_load_file) RAND_load_file; +extern int RAND_load_file(const char *, long); +%rename(rand_save_file) RAND_write_file; +extern int RAND_write_file(const char *); +%rename(rand_poll) RAND_poll; +extern int RAND_poll(void); +%rename(rand_status) RAND_status; +extern int RAND_status(void); +%rename(rand_cleanup) RAND_cleanup; +extern void RAND_cleanup(void); + +%inline %{ +static PyObject *_rand_err; + +void rand_init(PyObject *rand_err) { + Py_INCREF(rand_err); + _rand_err = rand_err; +} + +PyObject *rand_seed(PyObject *seed) { + const void *buf; + int len; + + if (m2_PyObject_AsReadBufferInt(seed, &buf, &len) == -1) + return NULL; + + RAND_seed(buf, len); + Py_INCREF(Py_None); + return Py_None; +} + +PyObject *rand_add(PyObject *blob, double entropy) { + const void *buf; + int len; + + if (m2_PyObject_AsReadBufferInt(blob, &buf, &len) == -1) + return NULL; + + RAND_add(buf, len, entropy); + Py_INCREF(Py_None); + return Py_None; +} + +PyObject *rand_bytes(int n) { + void *blob; + PyObject *obj; + + if (!(blob = PyMem_Malloc(n))) { + PyErr_SetString(PyExc_MemoryError, "rand_bytes"); + return NULL; + } + if (RAND_bytes(blob, n)) { + obj = PyString_FromStringAndSize(blob, n); + PyMem_Free(blob); + return obj; + } else { + PyMem_Free(blob); + Py_INCREF(Py_None); + return Py_None; + } +} + +PyObject *rand_pseudo_bytes(int n) { + int ret; + unsigned char *blob; + PyObject *tuple; + + if (!(blob=(unsigned char *)PyMem_Malloc(n))) { + PyErr_SetString(PyExc_MemoryError, "rand_pseudo_bytes"); + return NULL; + } + if (!(tuple=PyTuple_New(2))) { + PyErr_SetString(PyExc_RuntimeError, "PyTuple_New() fails"); + PyMem_Free(blob); + return NULL; + } + ret = RAND_pseudo_bytes(blob, n); + if (ret == -1) { + PyMem_Free(blob); + Py_DECREF(tuple); + Py_INCREF(Py_None); + return Py_None; + } else { + PyTuple_SET_ITEM(tuple, 0, PyString_FromStringAndSize((char*)blob, n)); + PyMem_Free(blob); + PyTuple_SET_ITEM(tuple, 1, PyInt_FromLong((long)ret)); + return tuple; + } +} + +void rand_screen(void) { +#ifdef __WINDOWS__ + RAND_screen(); +#endif +} + +int rand_win32_event(unsigned int imsg, int wparam, long lparam) { +#ifdef __WINDOWS__ + return RAND_event(imsg, wparam, lparam); +#else + return 0; +#endif +} +%} + +/* +2004-04-05, ngps: Still missing: + RAND_egd + RAND_egd_bytes + RAND_query_egd_bytes + RAND_file_name +*/ + + diff --git a/SWIG/_rc4.i b/SWIG/_rc4.i new file mode 100644 index 0000000..3259997 --- /dev/null +++ b/SWIG/_rc4.i @@ -0,0 +1,58 @@ +/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* Copyright (c) 1999 Ng Pheng Siong. All rights reserved. */ +/* $Id: _rc4.i 522 2007-05-08 22:21:51Z heikki $ */ + +%{ +#include <openssl/rc4.h> +%} + +%apply Pointer NONNULL { RC4_KEY * }; + +%inline %{ +RC4_KEY *rc4_new(void) { + RC4_KEY *key; + + if (!(key = (RC4_KEY *)PyMem_Malloc(sizeof(RC4_KEY)))) + PyErr_SetString(PyExc_MemoryError, "rc4_new"); + return key; +} + +void rc4_free(RC4_KEY *key) { + PyMem_Free((void *)key); +} + +PyObject *rc4_set_key(RC4_KEY *key, PyObject *value) { + const void *vbuf; + int vlen; + + if (m2_PyObject_AsReadBufferInt(value, &vbuf, &vlen) == -1) + return NULL; + + RC4_set_key(key, vlen, vbuf); + Py_INCREF(Py_None); + return Py_None; +} + +PyObject *rc4_update(RC4_KEY *key, PyObject *in) { + PyObject *ret; + const void *buf; + Py_ssize_t len; + void *out; + + if (PyObject_AsReadBuffer(in, &buf, &len) == -1) + return NULL; + + if (!(out = PyMem_Malloc(len))) { + PyErr_SetString(PyExc_MemoryError, "expected a string object"); + return NULL; + } + RC4(key, len, buf, out); + ret = PyString_FromStringAndSize(out, len); + PyMem_Free(out); + return ret; +} + +int rc4_type_check(RC4_KEY *key) { + return 1; +} +%} diff --git a/SWIG/_rsa.i b/SWIG/_rsa.i new file mode 100644 index 0000000..537e802 --- /dev/null +++ b/SWIG/_rsa.i @@ -0,0 +1,452 @@ +/* Copyright (c) 1999-2000 Ng Pheng Siong. All rights reserved. */ +/* $Id: _rsa.i 723 2010-02-13 06:53:13Z heikki $ */ + +%{ +#include <openssl/bn.h> +#include <openssl/err.h> +#include <openssl/pem.h> +#include <openssl/rsa.h> +#include <openssl/opensslv.h> +%} + +%apply Pointer NONNULL { RSA * }; +%apply Pointer NONNULL { PyObject *pyfunc }; + +%rename(rsa_new) RSA_new; +extern RSA *RSA_new(void); +%rename(rsa_free) RSA_free; +extern void RSA_free(RSA *); +%rename(rsa_size) RSA_size; +extern int RSA_size(const RSA *); +%rename(rsa_check_key) RSA_check_key; +extern int RSA_check_key(const RSA *); + +%constant int no_padding = RSA_NO_PADDING; +%constant int pkcs1_padding = RSA_PKCS1_PADDING; +%constant int sslv23_padding = RSA_SSLV23_PADDING; +%constant int pkcs1_oaep_padding = RSA_PKCS1_OAEP_PADDING; + +%constant int NID_sha1 = NID_sha1; + +#if OPENSSL_VERSION_NUMBER >= 0x0090800fL +%constant int NID_sha224 = NID_sha224; +%constant int NID_sha256 = NID_sha256; +%constant int NID_sha384 = NID_sha384; +%constant int NID_sha512 = NID_sha512; +#endif + +%constant int NID_md5 = NID_md5; + +%constant int NID_ripemd160 = NID_ripemd160; + +%inline %{ +static PyObject *_rsa_err; + +void rsa_init(PyObject *rsa_err) { + Py_INCREF(rsa_err); + _rsa_err = rsa_err; +} +%} + +%inline %{ +RSA *rsa_read_key(BIO *f, PyObject *pyfunc) { + RSA *rsa; + + Py_INCREF(pyfunc); + Py_BEGIN_ALLOW_THREADS + rsa = PEM_read_bio_RSAPrivateKey(f, NULL, passphrase_callback, (void *)pyfunc); + Py_END_ALLOW_THREADS + Py_DECREF(pyfunc); + return rsa; +} +%} + +%inline %{ +int rsa_write_key(RSA *rsa, BIO *f, EVP_CIPHER *cipher, PyObject *pyfunc) { + int ret; + + Py_INCREF(pyfunc); + Py_BEGIN_ALLOW_THREADS + ret = PEM_write_bio_RSAPrivateKey(f, rsa, cipher, NULL, 0, + passphrase_callback, (void *)pyfunc); + Py_END_ALLOW_THREADS + Py_DECREF(pyfunc); + return ret; +} +%} + +%inline %{ +int rsa_write_key_no_cipher(RSA *rsa, BIO *f, PyObject *pyfunc) { + int ret; + + Py_INCREF(pyfunc); + Py_BEGIN_ALLOW_THREADS + ret = PEM_write_bio_RSAPrivateKey(f, rsa, NULL, NULL, 0, + passphrase_callback, (void *)pyfunc); + Py_END_ALLOW_THREADS + Py_DECREF(pyfunc); + return ret; +} +%} + +%threadallow rsa_read_pub_key; +%inline %{ +RSA *rsa_read_pub_key(BIO *f) { + return PEM_read_bio_RSA_PUBKEY(f, NULL, NULL, NULL); +} +%} + +%threadallow rsa_write_pub_key; +%inline %{ +int rsa_write_pub_key(RSA *rsa, BIO *f) { + return PEM_write_bio_RSA_PUBKEY(f, rsa); +} + +PyObject *rsa_get_e(RSA *rsa) { + if (!rsa->e) { + PyErr_SetString(_rsa_err, "'e' is unset"); + return NULL; + } + return bn_to_mpi(rsa->e); +} + +PyObject *rsa_get_n(RSA *rsa) { + if (!rsa->n) { + PyErr_SetString(_rsa_err, "'n' is unset"); + return NULL; + } + return bn_to_mpi(rsa->n); +} + +PyObject *rsa_set_e(RSA *rsa, PyObject *value) { + BIGNUM *bn; + const void *vbuf; + int vlen; + + if (m2_PyObject_AsReadBufferInt(value, &vbuf, &vlen) == -1) + return NULL; + + if (!(bn = BN_mpi2bn((unsigned char *)vbuf, vlen, NULL))) { + PyErr_SetString(_rsa_err, ERR_reason_error_string(ERR_get_error())); + return NULL; + } + if (rsa->e) + BN_free(rsa->e); + rsa->e = bn; + Py_INCREF(Py_None); + return Py_None; +} + +PyObject *rsa_set_n(RSA *rsa, PyObject *value) { + BIGNUM *bn; + const void *vbuf; + int vlen; + + if (m2_PyObject_AsReadBufferInt(value, &vbuf, &vlen) == -1) + return NULL; + + if (!(bn = BN_mpi2bn((unsigned char *)vbuf, vlen, NULL))) { + PyErr_SetString(_rsa_err, ERR_reason_error_string(ERR_get_error())); + return NULL; + } + if (rsa->n) + BN_free(rsa->n); + rsa->n = bn; + Py_INCREF(Py_None); + return Py_None; +} + +PyObject *rsa_set_e_bin(RSA *rsa, PyObject *value) { + BIGNUM *bn; + const void *vbuf; + int vlen; + + if (m2_PyObject_AsReadBufferInt(value, &vbuf, &vlen) == -1) + return NULL; + + if (!(bn = BN_bin2bn((unsigned char *)vbuf, vlen, NULL))) { + PyErr_SetString(_rsa_err, ERR_reason_error_string(ERR_get_error())); + return NULL; + } + if (rsa->e) + BN_free(rsa->e); + rsa->e = bn; + Py_INCREF(Py_None); + return Py_None; +} + +PyObject *rsa_set_n_bin(RSA *rsa, PyObject *value) { + BIGNUM *bn; + const void *vbuf; + int vlen; + + if (m2_PyObject_AsReadBufferInt(value, &vbuf, &vlen) == -1) + return NULL; + + if (!(bn = BN_bin2bn((unsigned char *)vbuf, vlen, NULL))) { + PyErr_SetString(_rsa_err, ERR_reason_error_string(ERR_get_error())); + return NULL; + } + if (rsa->n) + BN_free(rsa->n); + rsa->n = bn; + Py_INCREF(Py_None); + return Py_None; +} + +PyObject *rsa_private_encrypt(RSA *rsa, PyObject *from, int padding) { + const void *fbuf; + void *tbuf; + int flen, tlen; + PyObject *ret; + + if (m2_PyObject_AsReadBufferInt(from, &fbuf, &flen) == -1) + return NULL; + + if (!(tbuf = PyMem_Malloc(BN_num_bytes(rsa->n)))) { + PyErr_SetString(PyExc_MemoryError, "rsa_private_encrypt"); + return NULL; + } + tlen = RSA_private_encrypt(flen, (unsigned char *)fbuf, + (unsigned char *)tbuf, rsa, padding); + if (tlen == -1) { + PyMem_Free(tbuf); + PyErr_SetString(_rsa_err, ERR_reason_error_string(ERR_get_error())); + return NULL; + } + ret = PyString_FromStringAndSize((const char *)tbuf, tlen); + PyMem_Free(tbuf); + return ret; +} + +PyObject *rsa_public_decrypt(RSA *rsa, PyObject *from, int padding) { + const void *fbuf; + void *tbuf; + int flen, tlen; + PyObject *ret; + + if (m2_PyObject_AsReadBufferInt(from, &fbuf, &flen) == -1) + return NULL; + + if (!(tbuf = PyMem_Malloc(BN_num_bytes(rsa->n)))) { + PyErr_SetString(PyExc_MemoryError, "rsa_public_decrypt"); + return NULL; + } + tlen = RSA_public_decrypt(flen, (unsigned char *)fbuf, + (unsigned char *)tbuf, rsa, padding); + if (tlen == -1) { + PyMem_Free(tbuf); + PyErr_SetString(_rsa_err, ERR_reason_error_string(ERR_get_error())); + return NULL; + } + ret = PyString_FromStringAndSize((const char *)tbuf, tlen); + PyMem_Free(tbuf); + return ret; +} + +PyObject *rsa_public_encrypt(RSA *rsa, PyObject *from, int padding) { + const void *fbuf; + void *tbuf; + int flen, tlen; + PyObject *ret; + + if (m2_PyObject_AsReadBufferInt(from, &fbuf, &flen) == -1) + return NULL; + + if (!(tbuf = PyMem_Malloc(BN_num_bytes(rsa->n)))) { + PyErr_SetString(PyExc_MemoryError, "rsa_public_encrypt"); + return NULL; + } + tlen = RSA_public_encrypt(flen, (unsigned char *)fbuf, + (unsigned char *)tbuf, rsa, padding); + if (tlen == -1) { + PyMem_Free(tbuf); + PyErr_SetString(_rsa_err, ERR_reason_error_string(ERR_get_error())); + return NULL; + } + ret = PyString_FromStringAndSize((const char *)tbuf, tlen); + PyMem_Free(tbuf); + return ret; +} + +PyObject *rsa_private_decrypt(RSA *rsa, PyObject *from, int padding) { + const void *fbuf; + void *tbuf; + int flen, tlen; + PyObject *ret; + + if (m2_PyObject_AsReadBufferInt(from, &fbuf, &flen) == -1) + return NULL; + + if (!(tbuf = PyMem_Malloc(BN_num_bytes(rsa->n)))) { + PyErr_SetString(PyExc_MemoryError, "rsa_private_decrypt"); + return NULL; + } + tlen = RSA_private_decrypt(flen, (unsigned char *)fbuf, + (unsigned char *)tbuf, rsa, padding); + if (tlen == -1) { + PyMem_Free(tbuf); + PyErr_SetString(_rsa_err, ERR_reason_error_string(ERR_get_error())); + return NULL; + } + ret = PyString_FromStringAndSize((const char *)tbuf, tlen); + PyMem_Free(tbuf); + return ret; +} + +#if OPENSSL_VERSION_NUMBER >= 0x0090708fL +PyObject *rsa_padding_add_pkcs1_pss(RSA *rsa, PyObject *digest, EVP_MD *hash, int salt_length) { + const void *dbuf; + unsigned char *tbuf; + int dlen, result, tlen; + PyObject *ret; + + if (m2_PyObject_AsReadBufferInt(digest, &dbuf, &dlen) == -1) + return NULL; + + tlen = RSA_size(rsa); + + if (!(tbuf = OPENSSL_malloc(tlen))) { + PyErr_SetString(PyExc_MemoryError, "rsa_padding_add_pkcs1_pss"); + return NULL; + } + result = RSA_padding_add_PKCS1_PSS( + rsa, + tbuf, + (unsigned char *)dbuf, + hash, + salt_length); + + if (result == -1) { + OPENSSL_cleanse(tbuf, tlen); + OPENSSL_free(tbuf); + PyErr_SetString(_rsa_err, ERR_reason_error_string(ERR_get_error())); + return NULL; + } + ret = PyString_FromStringAndSize((const char *)tbuf, tlen); + OPENSSL_cleanse(tbuf, tlen); + OPENSSL_free(tbuf); + return ret; +} + +int rsa_verify_pkcs1_pss(RSA *rsa, PyObject *digest, PyObject *signature, EVP_MD *hash, int salt_length) { + const void *dbuf; + const void *sbuf; + int dlen, slen, ret; + + if (m2_PyObject_AsReadBufferInt(digest, &dbuf, &dlen) == -1) { + return 0; + } + + if (m2_PyObject_AsReadBufferInt(signature, &sbuf, &slen) == -1) { + return 0; + } + + ret = RSA_verify_PKCS1_PSS( + rsa, + (unsigned char *)dbuf, + hash, + (unsigned char *)sbuf, + salt_length); + + return ret; +} +#endif + +PyObject *rsa_sign(RSA *rsa, PyObject *py_digest_string, int method_type) { + int digest_len = 0; + int buf_len = 0; + int ret = 0; + unsigned int real_buf_len = 0; + char *digest_string = NULL; + unsigned char * sign_buf = NULL; + PyObject *signature; + + ret = m2_PyString_AsStringAndSizeInt(py_digest_string, &digest_string, + &digest_len); + if (ret == -1) { + /* PyString_AsStringAndSize raises the correct exceptions. */ + return NULL; + } + + buf_len = RSA_size(rsa); + sign_buf = (unsigned char *)PyMem_Malloc(buf_len); + ret = RSA_sign(method_type, (const unsigned char *)digest_string, digest_len, + sign_buf, &real_buf_len, rsa); + + if (!ret) { + PyMem_Free(sign_buf); + PyErr_SetString(_rsa_err, ERR_reason_error_string(ERR_get_error())); + return NULL; + } + signature = PyString_FromStringAndSize((const char*) sign_buf, buf_len); + PyMem_Free(sign_buf); + return signature; +} + +int rsa_verify(RSA *rsa, PyObject *py_verify_string, PyObject* py_sign_string, int method_type){ + int ret = 0; + char * sign_string = NULL; + char * verify_string = NULL; + int verify_len = 0; + int sign_len = 0; + + ret = m2_PyString_AsStringAndSizeInt(py_verify_string, &verify_string, + &verify_len); + if (ret == -1) { + /* PyString_AsStringAndSize raises the correct exceptions. */ + return 0; + } + ret = m2_PyString_AsStringAndSizeInt(py_sign_string, &sign_string, + &sign_len); + if (ret == -1) { + return 0; + } + + ret = RSA_verify(method_type, (unsigned char *) verify_string, + verify_len, (unsigned char *) sign_string, + sign_len, rsa); + if (!ret) { + PyErr_SetString(_rsa_err, ERR_reason_error_string(ERR_get_error())); + } + return ret; +} + +void genrsa_callback(int p, int n, void *arg) { + PyObject *argv, *ret, *cbfunc; + + cbfunc = (PyObject *)arg; + argv = Py_BuildValue("(ii)", p, n); + ret = PyEval_CallObject(cbfunc, argv); + PyErr_Clear(); + Py_DECREF(argv); + Py_XDECREF(ret); +} + +RSA *rsa_generate_key(int bits, unsigned long e, PyObject *pyfunc) { + RSA *rsa; + + Py_INCREF(pyfunc); + rsa = RSA_generate_key(bits, e, genrsa_callback, (void *)pyfunc); + Py_DECREF(pyfunc); + if (!rsa) + PyErr_SetString(_rsa_err, ERR_reason_error_string(ERR_get_error())); + return rsa; +} + +int rsa_type_check(RSA *rsa) { + return 1; +} + +int rsa_check_pub_key(RSA *rsa) { + return (rsa->e) && (rsa->n); +} +%} + +%threadallow rsa_write_key_der; +%inline %{ +int rsa_write_key_der(RSA *rsa, BIO *bio) { + return i2d_RSAPrivateKey_bio(bio, rsa); +} +%} + diff --git a/SWIG/_ssl.i b/SWIG/_ssl.i new file mode 100644 index 0000000..2373ff2 --- /dev/null +++ b/SWIG/_ssl.i @@ -0,0 +1,739 @@ +/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* Copyright (c) 1999-2004 Ng Pheng Siong. All rights reserved. */ +/* +** Portions created by Open Source Applications Foundation (OSAF) are +** Copyright (C) 2004-2005 OSAF. All Rights Reserved. +** +** Copyright (c) 2009-2010 Heikki Toivonen. All rights reserved. +** +*/ +/* $Id: _ssl.i 721 2010-02-13 06:30:33Z heikki $ */ + +%{ +#include <pythread.h> +#include <openssl/bio.h> +#include <openssl/dh.h> +#include <openssl/ssl.h> +#include <openssl/x509.h> +%} + +%apply Pointer NONNULL { SSL_CTX * }; +%apply Pointer NONNULL { SSL * }; +%apply Pointer NONNULL { SSL_CIPHER * }; +%apply Pointer NONNULL { STACK_OF(SSL_CIPHER) * }; +%apply Pointer NONNULL { STACK_OF(X509) * }; +%apply Pointer NONNULL { BIO * }; +%apply Pointer NONNULL { DH * }; +%apply Pointer NONNULL { RSA * }; +%apply Pointer NONNULL { EVP_PKEY *}; +%apply Pointer NONNULL { PyObject *pyfunc }; + +%rename(ssl_get_ciphers) SSL_get_ciphers; +extern STACK_OF(SSL_CIPHER) *SSL_get_ciphers(const SSL *ssl); + +%rename(ssl_get_version) SSL_get_version; +extern const char *SSL_get_version(CONST SSL *); +%rename(ssl_get_error) SSL_get_error; +extern int SSL_get_error(CONST SSL *, int); +%rename(ssl_get_state) SSL_state_string; +extern const char *SSL_state_string(const SSL *); +%rename(ssl_get_state_v) SSL_state_string_long; +extern const char *SSL_state_string_long(const SSL *); +%rename(ssl_get_alert_type) SSL_alert_type_string; +extern const char *SSL_alert_type_string(int); +%rename(ssl_get_alert_type_v) SSL_alert_type_string_long; +extern const char *SSL_alert_type_string_long(int); +%rename(ssl_get_alert_desc) SSL_alert_desc_string; +extern const char *SSL_alert_desc_string(int); +%rename(ssl_get_alert_desc_v) SSL_alert_desc_string_long; +extern const char *SSL_alert_desc_string_long(int); + +%rename(sslv2_method) SSLv2_method; +extern SSL_METHOD *SSLv2_method(void); +%rename(sslv3_method) SSLv3_method; +extern SSL_METHOD *SSLv3_method(void); +%rename(sslv23_method) SSLv23_method; +extern SSL_METHOD *SSLv23_method(void); +%rename(tlsv1_method) TLSv1_method; +extern SSL_METHOD *TLSv1_method(void); + +%rename(ssl_ctx_new) SSL_CTX_new; +extern SSL_CTX *SSL_CTX_new(SSL_METHOD *); +%rename(ssl_ctx_free) SSL_CTX_free; +extern void SSL_CTX_free(SSL_CTX *); +%rename(ssl_ctx_set_verify_depth) SSL_CTX_set_verify_depth; +extern void SSL_CTX_set_verify_depth(SSL_CTX *, int); +%rename(ssl_ctx_get_verify_depth) SSL_CTX_get_verify_depth; +extern int SSL_CTX_get_verify_depth(CONST SSL_CTX *); +%rename(ssl_ctx_get_verify_mode) SSL_CTX_get_verify_mode; +extern int SSL_CTX_get_verify_mode(CONST SSL_CTX *); +%rename(ssl_ctx_set_cipher_list) SSL_CTX_set_cipher_list; +extern int SSL_CTX_set_cipher_list(SSL_CTX *, const char *); +%rename(ssl_ctx_add_session) SSL_CTX_add_session; +extern int SSL_CTX_add_session(SSL_CTX *, SSL_SESSION *); +%rename(ssl_ctx_remove_session) SSL_CTX_remove_session; +extern int SSL_CTX_remove_session(SSL_CTX *, SSL_SESSION *); +%rename(ssl_ctx_set_session_timeout) SSL_CTX_set_timeout; +extern long SSL_CTX_set_timeout(SSL_CTX *, long); +%rename(ssl_ctx_get_session_timeout) SSL_CTX_get_timeout; +extern long SSL_CTX_get_timeout(CONST SSL_CTX *); +%rename(ssl_ctx_get_cert_store) SSL_CTX_get_cert_store; +extern X509_STORE *SSL_CTX_get_cert_store(CONST SSL_CTX *); + +%rename(bio_new_ssl) BIO_new_ssl; +extern BIO *BIO_new_ssl(SSL_CTX *, int); + +%rename(ssl_new) SSL_new; +extern SSL *SSL_new(SSL_CTX *); +%rename(ssl_free) SSL_free; +%threadallow SSL_free; +extern void SSL_free(SSL *); +%rename(ssl_dup) SSL_dup; +extern SSL *SSL_dup(SSL *); +%rename(ssl_set_bio) SSL_set_bio; +extern void SSL_set_bio(SSL *, BIO *, BIO *); +%rename(ssl_set_accept_state) SSL_set_accept_state; +extern void SSL_set_accept_state(SSL *); +%rename(ssl_set_connect_state) SSL_set_connect_state; +extern void SSL_set_connect_state(SSL *); +%rename(ssl_get_shutdown) SSL_get_shutdown; +extern int SSL_get_shutdown(CONST SSL *); +%rename(ssl_set_shutdown) SSL_set_shutdown; +extern void SSL_set_shutdown(SSL *, int); +%rename(ssl_shutdown) SSL_shutdown; +%threadallow SSL_shutdown; +extern int SSL_shutdown(SSL *); +%rename(ssl_clear) SSL_clear; +extern int SSL_clear(SSL *); +%rename(ssl_do_handshake) SSL_do_handshake; +%threadallow SSL_do_handshake; +extern int SSL_do_handshake(SSL *); +%rename(ssl_renegotiate) SSL_renegotiate; +%threadallow SSL_renegotiate; +extern int SSL_renegotiate(SSL *); +%rename(ssl_pending) SSL_pending; +extern int SSL_pending(CONST SSL *); + +%rename(ssl_get_peer_cert) SSL_get_peer_certificate; +extern X509 *SSL_get_peer_certificate(CONST SSL *); +%rename(ssl_get_current_cipher) SSL_get_current_cipher; +extern SSL_CIPHER *SSL_get_current_cipher(CONST SSL *); +%rename(ssl_get_verify_mode) SSL_get_verify_mode; +extern int SSL_get_verify_mode(CONST SSL *); +%rename(ssl_get_verify_depth) SSL_get_verify_depth; +extern int SSL_get_verify_depth(CONST SSL *); +%rename(ssl_get_verify_result) SSL_get_verify_result; +extern long SSL_get_verify_result(CONST SSL *); +%rename(ssl_get_ssl_ctx) SSL_get_SSL_CTX; +extern SSL_CTX *SSL_get_SSL_CTX(CONST SSL *); +%rename(ssl_get_default_session_timeout) SSL_get_default_timeout; +extern long SSL_get_default_timeout(CONST SSL *); + +%rename(ssl_set_cipher_list) SSL_set_cipher_list; +extern int SSL_set_cipher_list(SSL *, const char *); +%rename(ssl_get_cipher_list) SSL_get_cipher_list; +extern const char *SSL_get_cipher_list(CONST SSL *, int); + +%rename(ssl_cipher_get_name) SSL_CIPHER_get_name; +extern const char *SSL_CIPHER_get_name(CONST SSL_CIPHER *); +%rename(ssl_cipher_get_version) SSL_CIPHER_get_version; +extern char *SSL_CIPHER_get_version(CONST SSL_CIPHER *); + +%rename(ssl_get_session) SSL_get_session; +extern SSL_SESSION *SSL_get_session(CONST SSL *); +%rename(ssl_get1_session) SSL_get1_session; +extern SSL_SESSION *SSL_get1_session(SSL *); +%rename(ssl_set_session) SSL_set_session; +extern int SSL_set_session(SSL *, SSL_SESSION *); +%rename(ssl_session_free) SSL_SESSION_free; +extern void SSL_SESSION_free(SSL_SESSION *); +%rename(ssl_session_print) SSL_SESSION_print; +%threadallow SSL_SESSION_print; +extern int SSL_SESSION_print(BIO *, CONST SSL_SESSION *); +%rename(ssl_session_set_timeout) SSL_SESSION_set_timeout; +extern long SSL_SESSION_set_timeout(SSL_SESSION *, long); +%rename(ssl_session_get_timeout) SSL_SESSION_get_timeout; +extern long SSL_SESSION_get_timeout(CONST SSL_SESSION *); + +%constant int ssl_error_none = SSL_ERROR_NONE; +%constant int ssl_error_ssl = SSL_ERROR_SSL; +%constant int ssl_error_want_read = SSL_ERROR_WANT_READ; +%constant int ssl_error_want_write = SSL_ERROR_WANT_WRITE; +%constant int ssl_error_want_x509_lookup = SSL_ERROR_WANT_X509_LOOKUP; +%constant int ssl_error_syscall = SSL_ERROR_SYSCALL; +%constant int ssl_error_zero_return = SSL_ERROR_ZERO_RETURN; +%constant int ssl_error_want_connect = SSL_ERROR_WANT_CONNECT; + +%constant int SSL_VERIFY_NONE = 0x00; +%constant int SSL_VERIFY_PEER = 0x01; +%constant int SSL_VERIFY_FAIL_IF_NO_PEER_CERT = 0x02; +%constant int SSL_VERIFY_CLIENT_ONCE = 0x04; + +%constant int SSL_ST_CONNECT = 0x1000; +%constant int SSL_ST_ACCEPT = 0x2000; +%constant int SSL_ST_MASK = 0x0FFF; +%constant int SSL_ST_INIT = (SSL_ST_CONNECT|SSL_ST_ACCEPT); +%constant int SSL_ST_BEFORE = 0x4000; +%constant int SSL_ST_OK = 0x03; +%constant int SSL_ST_RENEGOTIATE = (0x04|SSL_ST_INIT); + +%constant int SSL_CB_LOOP = 0x01; +%constant int SSL_CB_EXIT = 0x02; +%constant int SSL_CB_READ = 0x04; +%constant int SSL_CB_WRITE = 0x08; +%constant int SSL_CB_ALERT = 0x4000; /* used in callback */ +%constant int SSL_CB_READ_ALERT = (SSL_CB_ALERT|SSL_CB_READ); +%constant int SSL_CB_WRITE_ALERT = (SSL_CB_ALERT|SSL_CB_WRITE); +%constant int SSL_CB_ACCEPT_LOOP = (SSL_ST_ACCEPT|SSL_CB_LOOP); +%constant int SSL_CB_ACCEPT_EXIT = (SSL_ST_ACCEPT|SSL_CB_EXIT); +%constant int SSL_CB_CONNECT_LOOP = (SSL_ST_CONNECT|SSL_CB_LOOP); +%constant int SSL_CB_CONNECT_EXIT = (SSL_ST_CONNECT|SSL_CB_EXIT); +%constant int SSL_CB_HANDSHAKE_START = 0x10; +%constant int SSL_CB_HANDSHAKE_DONE = 0x20; + +%constant int SSL_SENT_SHUTDOWN = 1; +%constant int SSL_RECEIVED_SHUTDOWN = 2; + +%constant int SSL_SESS_CACHE_OFF = 0x000; +%constant int SSL_SESS_CACHE_CLIENT = 0x001; +%constant int SSL_SESS_CACHE_SERVER = 0x002; +%constant int SSL_SESS_CACHE_BOTH = (SSL_SESS_CACHE_CLIENT|SSL_SESS_CACHE_SERVER); + +%constant int SSL_OP_ALL = 0x00000FFFL; + +%constant int SSL_OP_NO_SSLv2 = 0x01000000L; +%constant int SSL_OP_NO_SSLv3 = 0x02000000L; +%constant int SSL_OP_NO_TLSv1 = 0x04000000L; +%constant int SSL_OP_DONT_INSERT_EMPTY_FRAGMENTS = 0x00000800L; + +%constant int SSL_MODE_ENABLE_PARTIAL_WRITE = SSL_MODE_ENABLE_PARTIAL_WRITE; +%constant int SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER = SSL_MODE_ENABLE_PARTIAL_WRITE; +%constant int SSL_MODE_AUTO_RETRY = SSL_MODE_AUTO_RETRY; + +%inline %{ +static PyObject *_ssl_err; + +void ssl_init(PyObject *ssl_err) { + SSL_library_init(); + SSL_load_error_strings(); + Py_INCREF(ssl_err); + _ssl_err = ssl_err; +} + +void ssl_ctx_passphrase_callback(SSL_CTX *ctx, PyObject *pyfunc) { + SSL_CTX_set_default_passwd_cb(ctx, passphrase_callback); + SSL_CTX_set_default_passwd_cb_userdata(ctx, (void *)pyfunc); + Py_INCREF(pyfunc); +} + +int ssl_ctx_use_x509(SSL_CTX *ctx, X509 *x) { + int i; + + if (!(i = SSL_CTX_use_certificate(ctx, x))) { + PyErr_SetString(_ssl_err, ERR_reason_error_string(ERR_get_error())); + return -1; + } + return i; + +} + +int ssl_ctx_use_cert(SSL_CTX *ctx, char *file) { + int i; + + if (!(i = SSL_CTX_use_certificate_file(ctx, file, SSL_FILETYPE_PEM))) { + PyErr_SetString(_ssl_err, ERR_reason_error_string(ERR_get_error())); + return -1; + } + return i; +} + +int ssl_ctx_use_cert_chain(SSL_CTX *ctx, char *file) { + int i; + + if (!(i = SSL_CTX_use_certificate_chain_file(ctx, file))) { + PyErr_SetString(_ssl_err, ERR_reason_error_string(ERR_get_error())); + return -1; + } + return i; +} + + +int ssl_ctx_use_privkey(SSL_CTX *ctx, char *file) { + int i; + + if (!(i = SSL_CTX_use_PrivateKey_file(ctx, file, SSL_FILETYPE_PEM))) { + PyErr_SetString(_ssl_err, ERR_reason_error_string(ERR_get_error())); + return -1; + } + return i; +} + +int ssl_ctx_use_rsa_privkey(SSL_CTX *ctx, RSA *rsakey) { + int i; + + if (!(i = SSL_CTX_use_RSAPrivateKey(ctx, rsakey))) { + PyErr_SetString(_ssl_err, ERR_reason_error_string(ERR_get_error())); + return -1; + } + return i; +} + +int ssl_ctx_use_pkey_privkey(SSL_CTX *ctx, EVP_PKEY *pkey) { + int i; + + if (!(i = SSL_CTX_use_PrivateKey(ctx, pkey))) { + PyErr_SetString(_ssl_err, ERR_reason_error_string(ERR_get_error())); + return -1; + } + return i; +} + + +int ssl_ctx_check_privkey(SSL_CTX *ctx) { + int ret; + + if (!(ret = SSL_CTX_check_private_key(ctx))) { + PyErr_SetString(_ssl_err, ERR_reason_error_string(ERR_get_error())); + return -1; + } + return ret; +} + +void ssl_ctx_set_client_CA_list_from_file(SSL_CTX *ctx, const char *ca_file) { + SSL_CTX_set_client_CA_list(ctx, SSL_load_client_CA_file(ca_file)); +} + +void ssl_ctx_set_verify_default(SSL_CTX *ctx, int mode) { + SSL_CTX_set_verify(ctx, mode, NULL); +} + +void ssl_ctx_set_verify(SSL_CTX *ctx, int mode, PyObject *pyfunc) { + Py_XDECREF(ssl_verify_cb_func); + Py_INCREF(pyfunc); + ssl_verify_cb_func = pyfunc; + SSL_CTX_set_verify(ctx, mode, ssl_verify_callback); +} + +int ssl_ctx_set_session_id_context(SSL_CTX *ctx, PyObject *sid_ctx) { + const void *buf; + int len; + + if (m2_PyObject_AsReadBufferInt(sid_ctx, &buf, &len) == -1) + return -1; + + return SSL_CTX_set_session_id_context(ctx, buf, len); +} + +void ssl_ctx_set_info_callback(SSL_CTX *ctx, PyObject *pyfunc) { + Py_XDECREF(ssl_info_cb_func); + Py_INCREF(pyfunc); + ssl_info_cb_func = pyfunc; + SSL_CTX_set_info_callback(ctx, ssl_info_callback); +} + +long ssl_ctx_set_tmp_dh(SSL_CTX *ctx, DH* dh) { + return SSL_CTX_set_tmp_dh(ctx, dh); +} + +void ssl_ctx_set_tmp_dh_callback(SSL_CTX *ctx, PyObject *pyfunc) { + Py_XDECREF(ssl_set_tmp_dh_cb_func); + Py_INCREF(pyfunc); + ssl_set_tmp_dh_cb_func = pyfunc; + SSL_CTX_set_tmp_dh_callback(ctx, ssl_set_tmp_dh_callback); +} + +long ssl_ctx_set_tmp_rsa(SSL_CTX *ctx, RSA* rsa) { + return SSL_CTX_set_tmp_rsa(ctx, rsa); +} + +void ssl_ctx_set_tmp_rsa_callback(SSL_CTX *ctx, PyObject *pyfunc) { + Py_XDECREF(ssl_set_tmp_rsa_cb_func); + Py_INCREF(pyfunc); + ssl_set_tmp_rsa_cb_func = pyfunc; + SSL_CTX_set_tmp_rsa_callback(ctx, ssl_set_tmp_rsa_callback); +} + +int ssl_ctx_load_verify_locations(SSL_CTX *ctx, const char *cafile, const char *capath) { + return SSL_CTX_load_verify_locations(ctx, cafile, capath); +} + +/* SSL_CTX_set_options is a macro. */ +long ssl_ctx_set_options(SSL_CTX *ctx, long op) { + return SSL_CTX_set_options(ctx, op); +} + +int bio_set_ssl(BIO *bio, SSL *ssl, int flag) { + SSL_set_mode(ssl, SSL_MODE_AUTO_RETRY); + return BIO_ctrl(bio, BIO_C_SET_SSL, flag, (char *)ssl); +} + +long ssl_set_mode(SSL *ssl, long mode) { + return SSL_set_mode(ssl, mode); +} + +long ssl_get_mode(SSL *ssl) { + return SSL_get_mode(ssl); +} + +void ssl_set_client_CA_list_from_file(SSL *ssl, const char *ca_file) { + SSL_set_client_CA_list(ssl, SSL_load_client_CA_file(ca_file)); +} + +void ssl_set_client_CA_list_from_context(SSL *ssl, SSL_CTX *ctx) { + SSL_set_client_CA_list(ssl, SSL_CTX_get_client_CA_list(ctx)); +} + +int ssl_set_session_id_context(SSL *ssl, PyObject *sid_ctx) { + const void *buf; + int len; + + if (m2_PyObject_AsReadBufferInt(sid_ctx, &buf, &len) == -1) + return -1; + + return SSL_set_session_id_context(ssl, buf, len); +} + +int ssl_set_fd(SSL *ssl, int fd) { + int ret; + + if (!(ret = SSL_set_fd(ssl, fd))) { + PyErr_SetString(_ssl_err, ERR_reason_error_string(ERR_get_error())); + return -1; + } + return ret; +} + +PyObject *ssl_accept(SSL *ssl) { + PyObject *obj = NULL; + int r, err; + + Py_BEGIN_ALLOW_THREADS + r = SSL_accept(ssl); + Py_END_ALLOW_THREADS + + + switch (SSL_get_error(ssl, r)) { + case SSL_ERROR_NONE: + case SSL_ERROR_ZERO_RETURN: + obj = PyInt_FromLong((long)1); + break; + case SSL_ERROR_WANT_WRITE: + case SSL_ERROR_WANT_READ: + obj = PyInt_FromLong((long)0); + break; + case SSL_ERROR_SSL: + PyErr_SetString(_ssl_err, ERR_reason_error_string(ERR_get_error())); + obj = NULL; + break; + case SSL_ERROR_SYSCALL: + err = ERR_get_error(); + if (err) + PyErr_SetString(_ssl_err, ERR_reason_error_string(err)); + else if (r == 0) + PyErr_SetString(_ssl_err, "unexpected eof"); + else if (r == -1) + PyErr_SetFromErrno(_ssl_err); + obj = NULL; + break; + } + + + return obj; +} + +PyObject *ssl_connect(SSL *ssl) { + PyObject *obj = NULL; + int r, err; + + Py_BEGIN_ALLOW_THREADS + r = SSL_connect(ssl); + Py_END_ALLOW_THREADS + + + switch (SSL_get_error(ssl, r)) { + case SSL_ERROR_NONE: + case SSL_ERROR_ZERO_RETURN: + obj = PyInt_FromLong((long)1); + break; + case SSL_ERROR_WANT_WRITE: + case SSL_ERROR_WANT_READ: + obj = PyInt_FromLong((long)0); + break; + case SSL_ERROR_SSL: + PyErr_SetString(_ssl_err, ERR_reason_error_string(ERR_get_error())); + obj = NULL; + break; + case SSL_ERROR_SYSCALL: + err = ERR_get_error(); + if (err) + PyErr_SetString(_ssl_err, ERR_reason_error_string(err)); + else if (r == 0) + PyErr_SetString(_ssl_err, "unexpected eof"); + else if (r == -1) + PyErr_SetFromErrno(_ssl_err); + obj = NULL; + break; + } + + + return obj; +} + +void ssl_set_shutdown1(SSL *ssl, int mode) { + SSL_set_shutdown(ssl, mode); +} + +PyObject *ssl_read(SSL *ssl, int num) { + PyObject *obj = NULL; + void *buf; + int r, err; + + if (!(buf = PyMem_Malloc(num))) { + PyErr_SetString(PyExc_MemoryError, "ssl_read"); + return NULL; + } + + + Py_BEGIN_ALLOW_THREADS + r = SSL_read(ssl, buf, num); + Py_END_ALLOW_THREADS + + + switch (SSL_get_error(ssl, r)) { + case SSL_ERROR_NONE: + case SSL_ERROR_ZERO_RETURN: + buf = PyMem_Realloc(buf, r); + obj = PyString_FromStringAndSize(buf, r); + break; + case SSL_ERROR_WANT_WRITE: + case SSL_ERROR_WANT_READ: + case SSL_ERROR_WANT_X509_LOOKUP: + Py_INCREF(Py_None); + obj = Py_None; + break; + case SSL_ERROR_SSL: + PyErr_SetString(_ssl_err, ERR_reason_error_string(ERR_get_error())); + obj = NULL; + break; + case SSL_ERROR_SYSCALL: + err = ERR_get_error(); + if (err) + PyErr_SetString(_ssl_err, ERR_reason_error_string(err)); + else if (r == 0) + PyErr_SetString(_ssl_err, "unexpected eof"); + else if (r == -1) + PyErr_SetFromErrno(_ssl_err); + obj = NULL; + break; + } + PyMem_Free(buf); + + + return obj; +} + +PyObject *ssl_read_nbio(SSL *ssl, int num) { + PyObject *obj = NULL; + void *buf; + int r, err; + + + if (!(buf = PyMem_Malloc(num))) { + PyErr_SetString(PyExc_MemoryError, "ssl_read"); + return NULL; + } + + + Py_BEGIN_ALLOW_THREADS + r = SSL_read(ssl, buf, num); + Py_END_ALLOW_THREADS + + + switch (SSL_get_error(ssl, r)) { + case SSL_ERROR_NONE: + case SSL_ERROR_ZERO_RETURN: + buf = PyMem_Realloc(buf, r); + obj = PyString_FromStringAndSize(buf, r); + break; + case SSL_ERROR_WANT_WRITE: + case SSL_ERROR_WANT_READ: + case SSL_ERROR_WANT_X509_LOOKUP: + Py_INCREF(Py_None); + obj = Py_None; + break; + case SSL_ERROR_SSL: + PyErr_SetString(_ssl_err, ERR_reason_error_string(ERR_get_error())); + obj = NULL; + break; + case SSL_ERROR_SYSCALL: + err = ERR_get_error(); + if (err) + PyErr_SetString(_ssl_err, ERR_reason_error_string(err)); + else if (r == 0) + PyErr_SetString(_ssl_err, "unexpected eof"); + else if (r == -1) + PyErr_SetFromErrno(_ssl_err); + obj = NULL; + break; + } + PyMem_Free(buf); + + + return obj; +} + +int ssl_write(SSL *ssl, PyObject *blob) { + const void *buf; + int len, r, err, ret; + + + if (m2_PyObject_AsReadBufferInt(blob, &buf, &len) == -1) { + return -1; + } + + + Py_BEGIN_ALLOW_THREADS + r = SSL_write(ssl, buf, len); + Py_END_ALLOW_THREADS + + + switch (SSL_get_error(ssl, r)) { + case SSL_ERROR_NONE: + case SSL_ERROR_ZERO_RETURN: + ret = r; + break; + case SSL_ERROR_WANT_WRITE: + case SSL_ERROR_WANT_READ: + case SSL_ERROR_WANT_X509_LOOKUP: + ret = -1; + break; + case SSL_ERROR_SSL: + PyErr_SetString(_ssl_err, ERR_reason_error_string(ERR_get_error())); + ret = -1; + break; + case SSL_ERROR_SYSCALL: + err = ERR_get_error(); + if (err) + PyErr_SetString(_ssl_err, ERR_reason_error_string(ERR_get_error())); + else if (r == 0) + PyErr_SetString(_ssl_err, "unexpected eof"); + else if (r == -1) + PyErr_SetFromErrno(_ssl_err); + default: + ret = -1; + } + + + return ret; +} + +int ssl_write_nbio(SSL *ssl, PyObject *blob) { + const void *buf; + int len, r, err, ret; + + + if (m2_PyObject_AsReadBufferInt(blob, &buf, &len) == -1) { + return -1; + } + + + Py_BEGIN_ALLOW_THREADS + r = SSL_write(ssl, buf, len); + Py_END_ALLOW_THREADS + + + switch (SSL_get_error(ssl, r)) { + case SSL_ERROR_NONE: + case SSL_ERROR_ZERO_RETURN: + ret = r; + break; + case SSL_ERROR_WANT_WRITE: + case SSL_ERROR_WANT_READ: + case SSL_ERROR_WANT_X509_LOOKUP: + ret = -1; + break; + case SSL_ERROR_SSL: + ret = -1; + break; + case SSL_ERROR_SYSCALL: + err = ERR_get_error(); + if (err) + PyErr_SetString(_ssl_err, ERR_reason_error_string(err)); + else if (r == 0) + PyErr_SetString(_ssl_err, "unexpected eof"); + else if (r == -1) + PyErr_SetFromErrno(_ssl_err); + default: + ret = -1; + } + + + return ret; +} + +int ssl_cipher_get_bits(SSL_CIPHER *c) { + return SSL_CIPHER_get_bits(c, NULL); +} + +int sk_ssl_cipher_num(STACK_OF(SSL_CIPHER) *stack) { + return sk_SSL_CIPHER_num(stack); +} + +SSL_CIPHER *sk_ssl_cipher_value(STACK_OF(SSL_CIPHER) *stack, int idx) { + return sk_SSL_CIPHER_value(stack, idx); +} + +STACK_OF(X509) *ssl_get_peer_cert_chain(SSL *ssl) { + return SSL_get_peer_cert_chain(ssl); +} + +int sk_x509_num(STACK_OF(X509) *stack) { + return sk_X509_num(stack); +} + +X509 *sk_x509_value(STACK_OF(X509) *stack, int idx) { + return sk_X509_value(stack, idx); +} +%} + +%threadallow i2d_ssl_session; +%inline %{ +void i2d_ssl_session(BIO *bio, SSL_SESSION *sess) { + i2d_SSL_SESSION_bio(bio, sess); +} +%} + +%threadallow ssl_session_read_pem; +%inline %{ +SSL_SESSION *ssl_session_read_pem(BIO *bio) { + return PEM_read_bio_SSL_SESSION(bio, NULL, NULL, NULL); +} +%} + +%threadallow ssl_session_write_pem; +%inline %{ +int ssl_session_write_pem(SSL_SESSION *sess, BIO *bio) { + return PEM_write_bio_SSL_SESSION(bio, sess); +} + +int ssl_ctx_set_session_cache_mode(SSL_CTX *ctx, int mode) +{ + return SSL_CTX_set_session_cache_mode(ctx, mode); +} + +int ssl_ctx_get_session_cache_mode(SSL_CTX *ctx) +{ + return SSL_CTX_get_session_cache_mode(ctx); +} + +static long ssl_ctx_set_cache_size(SSL_CTX *ctx, long arg) +{ + return SSL_CTX_sess_set_cache_size(ctx, arg); +} + +int ssl_is_init_finished(SSL *ssl) +{ + return SSL_is_init_finished(ssl); +} +%} + diff --git a/SWIG/_threads.i b/SWIG/_threads.i new file mode 100644 index 0000000..bb625df --- /dev/null +++ b/SWIG/_threads.i @@ -0,0 +1,66 @@ +/* Copyright (c) 1999 Ng Pheng Siong. All rights reserved. */ +/* $Id: _threads.i 690 2009-07-22 08:32:43Z heikki $ */ + +%{ +#include <pythread.h> +#include <openssl/crypto.h> + +#ifdef THREADING +static PyThread_type_lock lock_cs[CRYPTO_NUM_LOCKS]; +static long lock_count[CRYPTO_NUM_LOCKS]; +static int thread_mode = 0; +#endif + +void threading_locking_callback(int mode, int type, const char *file, int line) { +#ifdef THREADING + if (mode & CRYPTO_LOCK) { + PyThread_acquire_lock(lock_cs[type], WAIT_LOCK); + lock_count[type]++; + } else { + PyThread_release_lock(lock_cs[type]); + lock_count[type]--; + } +#endif +} + +unsigned long threading_id_callback(void) { +#ifdef THREADING + return (unsigned long)PyThread_get_thread_ident(); +#else + return (unsigned long)0; +#endif +} +%} + +%inline %{ +void threading_init(void) { +#ifdef THREADING + int i; + if (!thread_mode) { + for (i=0; i<CRYPTO_NUM_LOCKS; i++) { + lock_count[i]=0; + lock_cs[i]=PyThread_allocate_lock(); + } + CRYPTO_set_id_callback(threading_id_callback); + CRYPTO_set_locking_callback(threading_locking_callback); + } + thread_mode = 1; +#endif +} + +void threading_cleanup(void) { +#ifdef THREADING + int i; + if (thread_mode) { + CRYPTO_set_locking_callback(NULL); + for (i=0; i<CRYPTO_NUM_LOCKS; i++) { + lock_count[i]=0; + PyThread_release_lock(lock_cs[i]); + PyThread_free_lock(lock_cs[i]); + } + } + thread_mode = 0; +#endif +} +%} + diff --git a/SWIG/_util.i b/SWIG/_util.i new file mode 100644 index 0000000..522b8c8 --- /dev/null +++ b/SWIG/_util.i @@ -0,0 +1,57 @@ +/* Copyright (c) 1999-2002 Ng Pheng Siong. All rights reserved. + * Copyright (c) 2009-2010 Heikki Toivonen. All rights reserved. +*/ +/* $Id: _util.i 721 2010-02-13 06:30:33Z heikki $ */ + +%{ +#include <openssl/x509v3.h> +%} + +%inline %{ +static PyObject *_util_err; + +void util_init(PyObject *util_err) { + Py_INCREF(util_err); + _util_err = util_err; +} + +PyObject *util_hex_to_string(PyObject *blob) { + PyObject *obj; + const void *buf; + char *ret; + Py_ssize_t len; + + if (PyObject_AsReadBuffer(blob, &buf, &len) == -1) + return NULL; + + ret = hex_to_string((unsigned char *)buf, len); + if (!ret) { + PyErr_SetString(_util_err, ERR_reason_error_string(ERR_get_error())); + return NULL; + } + obj = PyString_FromString(ret); + OPENSSL_free(ret); + return obj; +} + +PyObject *util_string_to_hex(PyObject *blob) { + PyObject *obj; + const void *buf; + unsigned char *ret; + Py_ssize_t len0; + long len; + + if (PyObject_AsReadBuffer(blob, &buf, &len0) == -1) + return NULL; + + len = len0; + ret = string_to_hex((char *)buf, &len); + if (ret == NULL) { + PyErr_SetString(_util_err, ERR_reason_error_string(ERR_get_error())); + return NULL; + } + obj = PyString_FromStringAndSize((char*)ret, len); + OPENSSL_free(ret); + return obj; +} +%} diff --git a/SWIG/_x509.i b/SWIG/_x509.i new file mode 100644 index 0000000..0471f68 --- /dev/null +++ b/SWIG/_x509.i @@ -0,0 +1,674 @@ +/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* Copyright (c) 1999-2004 Ng Pheng Siong. All rights reserved. */ +/* +** Portions created by Open Source Applications Foundation (OSAF) are +** Copyright (C) 2004-2005 OSAF. All Rights Reserved. +** +** Copyright (c) 2009-2010 Heikki Toivonen. All rights reserved. +** +*/ +/* $Id: _x509.i 721 2010-02-13 06:30:33Z heikki $ */ + +%{ +#include <openssl/x509.h> +#include <openssl/x509v3.h> +%} + +%apply Pointer NONNULL { BIO * }; +%apply Pointer NONNULL { X509 * }; +%apply Pointer NONNULL { X509_CRL * }; +%apply Pointer NONNULL { X509_REQ * }; +%apply Pointer NONNULL { X509_NAME * }; +%apply Pointer NONNULL { X509_NAME_ENTRY * }; +%apply Pointer NONNULL { EVP_PKEY * }; + +#if OPENSSL_VERSION_NUMBER >= 0x0090800fL +%rename(x509_check_ca) X509_check_ca; +extern int X509_check_ca(X509 *); +#endif + +%rename(x509_new) X509_new; +extern X509 *X509_new( void ); +%rename(x509_dup) X509_dup; +extern X509 *X509_dup(X509 *); +%rename(x509_free) X509_free; +extern void X509_free(X509 *); +%rename(x509_crl_free) X509_CRL_free; +extern void X509_CRL_free(X509_CRL *); +%rename(x509_crl_new) X509_CRL_new; +extern X509_CRL * X509_CRL_new(); + +%rename(x509_print) X509_print; +%threadallow X509_print; +extern int X509_print(BIO *, X509 *); +%rename(x509_crl_print) X509_CRL_print; +%threadallow X509_CRL_print; +extern int X509_CRL_print(BIO *, X509_CRL *); + +%rename(x509_get_serial_number) X509_get_serialNumber; +extern ASN1_INTEGER *X509_get_serialNumber(X509 *); +%rename(x509_set_serial_number) X509_set_serialNumber; +extern int X509_set_serialNumber(X509 *, ASN1_INTEGER *); +%rename(x509_get_pubkey) X509_get_pubkey; +extern EVP_PKEY *X509_get_pubkey(X509 *); +%rename(x509_set_pubkey) X509_set_pubkey; +extern int X509_set_pubkey(X509 *, EVP_PKEY *); +%rename(x509_get_issuer_name) X509_get_issuer_name; +extern X509_NAME *X509_get_issuer_name(X509 *); +%rename(x509_set_issuer_name) X509_set_issuer_name; +extern int X509_set_issuer_name(X509 *, X509_NAME *); +%rename(x509_get_subject_name) X509_get_subject_name; +extern X509_NAME *X509_get_subject_name(X509 *); +%rename(x509_set_subject_name) X509_set_subject_name; +extern int X509_set_subject_name(X509 *, X509_NAME *); +%rename(x509_cmp_current_time) X509_cmp_current_time; +extern int X509_cmp_current_time(ASN1_UTCTIME *); + + +/* From x509.h */ +/* standard trust ids */ +%constant int X509_TRUST_DEFAULT = -1; +%constant int X509_TRUST_COMPAT = 1; +%constant int X509_TRUST_SSL_CLIENT = 2; +%constant int X509_TRUST_SSL_SERVER = 3; +%constant int X509_TRUST_EMAIL = 4; +%constant int X509_TRUST_OBJECT_SIGN = 5; +%constant int X509_TRUST_OCSP_SIGN = 6; +%constant int X509_TRUST_OCSP_REQUEST = 7; + +/* trust_flags values */ +%constant int X509_TRUST_DYNAMIC = 1; +%constant int X509_TRUST_DYNAMIC_NAME = 2; + +/* check_trust return codes */ +%constant int X509_TRUST_TRUSTED = 1; +%constant int X509_TRUST_REJECTED = 2; +%constant int X509_TRUST_UNTRUSTED = 3; + +/* From x509v3.h */ +%constant int X509_PURPOSE_SSL_CLIENT = 1; +%constant int X509_PURPOSE_SSL_SERVER = 2; +%constant int X509_PURPOSE_NS_SSL_SERVER = 3; +%constant int X509_PURPOSE_SMIME_SIGN = 4; +%constant int X509_PURPOSE_SMIME_ENCRYPT = 5; +%constant int X509_PURPOSE_CRL_SIGN = 6; +%constant int X509_PURPOSE_ANY = 7; +%constant int X509_PURPOSE_OCSP_HELPER = 8; + +%rename(x509_check_purpose) X509_check_purpose; +extern int X509_check_purpose(X509 *, int, int); +%rename(x509_check_trust) X509_check_trust; +extern int X509_check_trust(X509 *, int, int); + +%rename(x509_write_pem) PEM_write_bio_X509; +%threadallow PEM_write_bio_X509; +extern int PEM_write_bio_X509(BIO *, X509 *); +%rename(x509_write_pem_file) PEM_write_X509; +extern int PEM_write_X509(FILE *, X509 *); + +%rename(x509_verify) X509_verify; +extern int X509_verify(X509 *a, EVP_PKEY *r); +%rename(x509_get_verify_error) X509_verify_cert_error_string; +extern const char *X509_verify_cert_error_string(long); + +%constant long X509V3_EXT_UNKNOWN_MASK = (0xfL << 16); +%constant long X509V3_EXT_DEFAULT = 0; +%constant long X509V3_EXT_ERROR_UNKNOWN = (1L << 16); +%constant long X509V3_EXT_PARSE_UNKNOWN = (2L << 16); +%constant long X509V3_EXT_DUMP_UNKNOWN = (3L << 16); + +%rename(x509_add_ext) X509_add_ext; +extern int X509_add_ext(X509 *, X509_EXTENSION *, int); +%rename(x509_get_ext_count) X509_get_ext_count; +extern int X509_get_ext_count(X509 *); +%rename(x509_get_ext) X509_get_ext; +extern X509_EXTENSION *X509_get_ext(X509 *, int); +%rename(x509_ext_print) X509V3_EXT_print; +%threadallow X509V3_EXT_print; +extern int X509V3_EXT_print(BIO *, X509_EXTENSION *, unsigned long, int); + +%rename(x509_name_new) X509_NAME_new; +extern X509_NAME *X509_NAME_new( void ); +%rename(x509_name_free) X509_NAME_free; +extern void X509_NAME_free(X509_NAME *); +%rename(x509_name_print) X509_NAME_print; +%threadallow X509_NAME_print; +extern int X509_NAME_print(BIO *, X509_NAME *, int); +%rename(x509_name_get_entry) X509_NAME_get_entry; +extern X509_NAME_ENTRY *X509_NAME_get_entry(X509_NAME *, int); +%rename(x509_name_entry_count) X509_NAME_entry_count; +extern int X509_NAME_entry_count(X509_NAME *); +%rename(x509_name_delete_entry) X509_NAME_delete_entry; +extern X509_NAME_ENTRY *X509_NAME_delete_entry(X509_NAME *, int); +%rename(x509_name_add_entry) X509_NAME_add_entry; +extern int X509_NAME_add_entry(X509_NAME *, X509_NAME_ENTRY *, int, int); +%rename(x509_name_add_entry_by_obj) X509_NAME_add_entry_by_OBJ; +extern int X509_NAME_add_entry_by_OBJ(X509_NAME *, ASN1_OBJECT *, int, unsigned char *, int, int, int ); +%rename(x509_name_add_entry_by_nid) X509_NAME_add_entry_by_NID; +extern int X509_NAME_add_entry_by_NID(X509_NAME *, int, int, unsigned char *, int, int, int ); +%rename(x509_name_print_ex) X509_NAME_print_ex; +%threadallow X509_NAME_print_ex; +extern int X509_NAME_print_ex(BIO *, X509_NAME *, int, unsigned long); +%rename(x509_name_print_ex_fp) X509_NAME_print_ex_fp; +extern int X509_NAME_print_ex_fp(FILE *, X509_NAME *, int, unsigned long); + +#if OPENSSL_VERSION_NUMBER >= 0x10000000L +%rename(x509_name_hash) X509_NAME_hash_old; +extern unsigned long X509_NAME_hash_old(X509_NAME *); +#else +%rename(x509_name_hash) X509_NAME_hash; +extern unsigned long X509_NAME_hash(X509_NAME *); +#endif + +%rename(x509_name_get_index_by_nid) X509_NAME_get_index_by_NID; +extern int X509_NAME_get_index_by_NID(X509_NAME *, int, int); + +%rename(x509_name_entry_new) X509_NAME_ENTRY_new; +extern X509_NAME_ENTRY *X509_NAME_ENTRY_new( void ); +%rename(x509_name_entry_free) X509_NAME_ENTRY_free; +extern void X509_NAME_ENTRY_free( X509_NAME_ENTRY *); +/*XXX This is probably bogus:*/ +%rename(x509_name_entry_create_by_nid) X509_NAME_ENTRY_create_by_NID; +extern X509_NAME_ENTRY *X509_NAME_ENTRY_create_by_NID( X509_NAME_ENTRY **, int, int, unsigned char *, int); +%rename(x509_name_entry_set_object) X509_NAME_ENTRY_set_object; +extern int X509_NAME_ENTRY_set_object( X509_NAME_ENTRY *, ASN1_OBJECT *); +%rename(x509_name_entry_get_object) X509_NAME_ENTRY_get_object; +extern ASN1_OBJECT *X509_NAME_ENTRY_get_object(X509_NAME_ENTRY *); +%rename(x509_name_entry_get_data) X509_NAME_ENTRY_get_data; +extern ASN1_STRING *X509_NAME_ENTRY_get_data(X509_NAME_ENTRY *); + +%typemap(in) (CONST unsigned char *, int) { + if (PyString_Check($input)) { + Py_ssize_t len; + + $1 = (unsigned char *)PyString_AsString($input); + len = PyString_Size($input); + if (len > INT_MAX) { + PyErr_SetString(PyExc_ValueError, "object too large"); + return NULL; + } + $2 = len; + } else { + PyErr_SetString(PyExc_TypeError, "expected string"); + return NULL; + } +} +%rename(x509_name_entry_set_data) X509_NAME_ENTRY_set_data; +extern int X509_NAME_ENTRY_set_data(X509_NAME_ENTRY *, int, CONST unsigned char *, int); +%typemap(in) (CONST unsigned char *, int); + +%rename(x509_req_new) X509_REQ_new; +extern X509_REQ * X509_REQ_new(); +%rename(x509_req_free) X509_REQ_free; +extern void X509_REQ_free(X509_REQ *); +%rename(x509_req_print) X509_REQ_print; +%threadallow X509_REQ_print; +extern int X509_REQ_print(BIO *, X509_REQ *); + +%rename(x509_req_get_pubkey) X509_REQ_get_pubkey; +extern EVP_PKEY *X509_REQ_get_pubkey(X509_REQ *); +%rename(x509_req_set_pubkey) X509_REQ_set_pubkey; +extern int X509_REQ_set_pubkey(X509_REQ *, EVP_PKEY *); +%rename(x509_req_set_subject_name) X509_REQ_set_subject_name; +extern int X509_REQ_set_subject_name(X509_REQ *, X509_NAME *); + +%rename(x509_req_verify) X509_REQ_verify; +extern int X509_REQ_verify(X509_REQ *, EVP_PKEY *); +%rename(x509_req_sign) X509_REQ_sign; +extern int X509_REQ_sign(X509_REQ *, EVP_PKEY *, const EVP_MD *); + +%rename(i2d_x509_bio) i2d_X509_bio; +%threadallow i2d_X509_bio; +extern int i2d_X509_bio(BIO *, X509 *); +%rename(i2d_x509_req_bio) i2d_X509_REQ_bio; +%threadallow i2d_X509_REQ_bio; +extern int i2d_X509_REQ_bio(BIO *, X509_REQ *); + +%rename(x509_store_new) X509_STORE_new; +extern X509_STORE *X509_STORE_new(void); +%rename(x509_store_free) X509_STORE_free; +extern void X509_STORE_free(X509_STORE *); +%rename(x509_store_add_cert) X509_STORE_add_cert; +extern int X509_STORE_add_cert(X509_STORE *, X509 *); + +%rename(x509_store_ctx_get_current_cert) X509_STORE_CTX_get_current_cert; +extern X509 *X509_STORE_CTX_get_current_cert(X509_STORE_CTX *); +%rename(x509_store_ctx_get_error) X509_STORE_CTX_get_error; +extern int X509_STORE_CTX_get_error(X509_STORE_CTX *); +%rename(x509_store_ctx_get_error_depth) X509_STORE_CTX_get_error_depth; +extern int X509_STORE_CTX_get_error_depth(X509_STORE_CTX *); +%rename(x509_store_ctx_free) X509_STORE_CTX_free; +extern void X509_STORE_CTX_free(X509_STORE_CTX *); +%rename(x509_store_ctx_get1_chain) X509_STORE_CTX_get1_chain; +extern STACK_OF(X509) *X509_STORE_CTX_get1_chain(X509_STORE_CTX *); + +%rename(x509_extension_get_critical) X509_EXTENSION_get_critical; +extern int X509_EXTENSION_get_critical(X509_EXTENSION *); +%rename(x509_extension_set_critical) X509_EXTENSION_set_critical; +extern int X509_EXTENSION_set_critical(X509_EXTENSION *, int); + + +%constant int NID_commonName = 13; +%constant int NID_countryName = 14; +%constant int NID_localityName = 15; +%constant int NID_stateOrProvinceName = 16; +%constant int NID_organizationName = 17; +%constant int NID_organizationalUnitName = 18; +%constant int NID_serialNumber = 105; +%constant int NID_surname = 100; +%constant int NID_givenName = 99; +%constant int NID_pkcs9_emailAddress = 48; + +/* Cribbed from x509_vfy.h. */ +%constant int X509_V_OK = 0; +%constant int X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT = 2; +%constant int X509_V_ERR_UNABLE_TO_GET_CRL = 3; +%constant int X509_V_ERR_UNABLE_TO_DECRYPT_CERT_SIGNATURE = 4; +%constant int X509_V_ERR_UNABLE_TO_DECRYPT_CRL_SIGNATURE = 5; +%constant int X509_V_ERR_UNABLE_TO_DECODE_ISSUER_PUBLIC_KEY = 6; +%constant int X509_V_ERR_CERT_SIGNATURE_FAILURE = 7; +%constant int X509_V_ERR_CRL_SIGNATURE_FAILURE = 8; +%constant int X509_V_ERR_CERT_NOT_YET_VALID = 9; +%constant int X509_V_ERR_CERT_HAS_EXPIRED = 10; +%constant int X509_V_ERR_CRL_NOT_YET_VALID = 11; +%constant int X509_V_ERR_CRL_HAS_EXPIRED = 12; +%constant int X509_V_ERR_ERROR_IN_CERT_NOT_BEFORE_FIELD = 13; +%constant int X509_V_ERR_ERROR_IN_CERT_NOT_AFTER_FIELD = 14; +%constant int X509_V_ERR_ERROR_IN_CRL_LAST_UPDATE_FIELD = 15; +%constant int X509_V_ERR_ERROR_IN_CRL_NEXT_UPDATE_FIELD = 16; +%constant int X509_V_ERR_OUT_OF_MEM = 17; +%constant int X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT = 18; +%constant int X509_V_ERR_SELF_SIGNED_CERT_IN_CHAIN = 19; +%constant int X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY = 20; +%constant int X509_V_ERR_UNABLE_TO_VERIFY_LEAF_SIGNATURE = 21; +%constant int X509_V_ERR_CERT_CHAIN_TOO_LONG = 22; +%constant int X509_V_ERR_CERT_REVOKED = 23; +%constant int X509_V_ERR_INVALID_CA = 24; +%constant int X509_V_ERR_PATH_LENGTH_EXCEEDED = 25; +%constant int X509_V_ERR_INVALID_PURPOSE = 26; +%constant int X509_V_ERR_CERT_UNTRUSTED = 27; +%constant int X509_V_ERR_CERT_REJECTED = 28; +%constant int X509_V_ERR_APPLICATION_VERIFICATION = 50; + +/* x509.h */ +%constant int XN_FLAG_COMPAT = 0; +%constant int XN_FLAG_SEP_COMMA_PLUS = (1 << 16); +%constant int XN_FLAG_SEP_CPLUS_SPC = (2 << 16); +%constant int XN_FLAG_SEP_MULTILINE = (4 << 16); +%constant int XN_FLAG_DN_REV = (1 << 20); +%constant int XN_FLAG_FN_LN = (1 << 21); +%constant int XN_FLAG_SPC_EQ = (1 << 23); +%constant int XN_FLAG_DUMP_UNKNOWN_FIELDS = (1 << 24); +%constant int XN_FLAG_FN_ALIGN = (1 << 25); +%constant int XN_FLAG_ONELINE =(ASN1_STRFLGS_RFC2253 | \ + ASN1_STRFLGS_ESC_QUOTE | \ + XN_FLAG_SEP_CPLUS_SPC | \ + XN_FLAG_SPC_EQ); +%constant int XN_FLAG_MULTILINE = (ASN1_STRFLGS_ESC_CTRL | \ + ASN1_STRFLGS_ESC_MSB | \ + XN_FLAG_SEP_MULTILINE | \ + XN_FLAG_SPC_EQ | \ + XN_FLAG_FN_LN | \ + XN_FLAG_FN_ALIGN); +%constant int XN_FLAG_RFC2253 = (ASN1_STRFLGS_RFC2253 | \ + XN_FLAG_SEP_COMMA_PLUS | \ + XN_FLAG_DN_REV | \ + XN_FLAG_DUMP_UNKNOWN_FIELDS); + +/* Cribbed from rsa.h. */ +%constant int RSA_3 = 0x3L; +%constant int RSA_F4 = 0x10001L; + +%inline %{ +static PyObject *_x509_err; + +void x509_init(PyObject *x509_err) { + Py_INCREF(x509_err); + _x509_err = x509_err; +} +%} + +%threadallow x509_read_pem; +%inline %{ +X509 *x509_read_pem(BIO *bio) { + return PEM_read_bio_X509(bio, NULL, NULL, NULL); +} +%} + +%threadallow d2i_x509; +%inline %{ +X509 *d2i_x509(BIO *bio) { + return d2i_X509_bio(bio, NULL); +} +%} + +%threadallow d2i_x509_req; +%inline %{ +X509_REQ *d2i_x509_req(BIO *bio) { + return d2i_X509_REQ_bio(bio, NULL); +} + +PyObject *i2d_x509(X509 *x) +{ + int len; + PyObject *ret = NULL; + unsigned char *buf = NULL; + len = i2d_X509(x, &buf); + if (len < 0) { + PyErr_SetString(_x509_err, ERR_reason_error_string(ERR_get_error())); + } + else { + ret = PyString_FromStringAndSize((char*)buf, len); + OPENSSL_free(buf); + } + return ret; +} +%} + +%threadallow x509_req_read_pem; +%inline %{ +X509_REQ *x509_req_read_pem(BIO *bio) { + return PEM_read_bio_X509_REQ(bio, NULL, NULL, NULL); +} +%} + +%threadallow x509_req_write_pem; +%inline %{ +int x509_req_write_pem(BIO *bio, X509_REQ *x) { + return PEM_write_bio_X509_REQ(bio, x); +} +%} + +%threadallow x509_crl_read_pem; +%inline %{ +X509_CRL *x509_crl_read_pem(BIO *bio) { + return PEM_read_bio_X509_CRL(bio, NULL, NULL, NULL); +} + +/* X509_set_version() is a macro. */ +int x509_set_version(X509 *x, long version) { + return X509_set_version(x, version); +} + +/* X509_get_version() is a macro. */ +long x509_get_version(X509 *x) { + return X509_get_version(x); +} + +/* X509_set_notBefore() is a macro. */ +int x509_set_not_before(X509 *x, ASN1_UTCTIME *tm) { + return X509_set_notBefore(x, tm); +} + +/* X509_get_notBefore() is a macro. */ +ASN1_UTCTIME *x509_get_not_before(X509 *x) { + return X509_get_notBefore(x); +} + +/* X509_set_notAfter() is a macro. */ +int x509_set_not_after(X509 *x, ASN1_UTCTIME *tm) { + return X509_set_notAfter(x, tm); +} + +/* X509_get_notAfter() is a macro. */ +ASN1_UTCTIME *x509_get_not_after(X509 *x) { + return X509_get_notAfter(x); +} + +int x509_sign(X509 *x, EVP_PKEY *pkey, EVP_MD *md) { + return X509_sign(x, pkey, md); +} + +/* XXX The first parameter is really ASN1_TIME, does it matter? */ +ASN1_TIME *x509_gmtime_adj(ASN1_UTCTIME *s, long adj) { + return X509_gmtime_adj(s, adj); +} + +PyObject *x509_name_by_nid(X509_NAME *name, int nid) { + void *buf; + int len, xlen; + PyObject *ret; + + if ((len = X509_NAME_get_text_by_NID(name, nid, NULL, 0)) == -1) { + Py_INCREF(Py_None); + return Py_None; + } + len++; + if (!(buf = PyMem_Malloc(len))) { + PyErr_SetString(PyExc_MemoryError, "x509_name_by_nid"); + return NULL; + } + xlen = X509_NAME_get_text_by_NID(name, nid, buf, len); + ret = PyString_FromStringAndSize(buf, xlen); + PyMem_Free(buf); + return ret; +} + +int x509_name_set_by_nid(X509_NAME *name, int nid, PyObject *obj) { + return X509_NAME_add_entry_by_NID(name, nid, MBSTRING_ASC, (unsigned char *)PyString_AsString(obj), -1, -1, 0); +} + +/* x509_name_add_entry_by_txt */ +int x509_name_add_entry_by_txt(X509_NAME *name, char *field, int type, char *bytes, int len, int loc, int set) { + return X509_NAME_add_entry_by_txt(name, field, type, (unsigned char *)bytes, len, loc, set); +} + +PyObject *x509_name_get_der(X509_NAME *name) +{ + i2d_X509_NAME(name, 0); + return PyString_FromStringAndSize(name->bytes->data, name->bytes->length); +} + +/* sk_X509_new_null() is a macro returning "STACK_OF(X509) *". */ +STACK_OF(X509) *sk_x509_new_null(void) { + return sk_X509_new_null(); +} + +/* sk_X509_free() is a macro. */ +void sk_x509_free(STACK_OF(X509) *stack) { + sk_X509_free(stack); +} + +/* sk_X509_push() is a macro. */ +int sk_x509_push(STACK_OF(X509) *stack, X509 *x509) { + return sk_X509_push(stack, x509); +} + +/* sk_X509_pop() is a macro. */ +X509 *sk_x509_pop(STACK_OF(X509) *stack) { + return sk_X509_pop(stack); +} + +int x509_store_load_locations(X509_STORE *store, const char *file) { + return X509_STORE_load_locations(store, file, NULL); +} + +int x509_type_check(X509 *x509) { + return 1; +} + +int x509_name_type_check(X509_NAME *name) { + return 1; +} + +X509_NAME *x509_req_get_subject_name(X509_REQ *x) { + return X509_REQ_get_subject_name(x); +} + +long x509_req_get_version(X509_REQ *x) { + return X509_REQ_get_version(x); +} + +int x509_req_set_version(X509_REQ *x, long version) { + return X509_REQ_set_version(x, version); +} + +int x509_req_add_extensions(X509_REQ *req, STACK_OF(X509_EXTENSION) *exts) { + return X509_REQ_add_extensions(req, exts); +} + +X509_NAME_ENTRY *x509_name_entry_create_by_txt(X509_NAME_ENTRY **ne, char *field, int type, char *bytes, int len) { + return X509_NAME_ENTRY_create_by_txt( ne, field, type, (unsigned char *)bytes, len); +} + +#if OPENSSL_VERSION_NUMBER >= 0x10000000L +LHASH_OF(CONF_VALUE) +#else +LHASH +#endif +*x509v3_lhash() { + return lh_new(NULL, NULL); /* Should probably be lh_CONF_VALUE_new but won't compile. */ +} + +X509V3_CTX * +#if OPENSSL_VERSION_NUMBER >= 0x10000000L +x509v3_set_conf_lhash(LHASH_OF(CONF_VALUE) * lhash) { +#else +x509v3_set_conf_lhash(LHASH * lhash) { +#endif + X509V3_CTX * ctx; + if (!(ctx=(X509V3_CTX *)PyMem_Malloc(sizeof(X509V3_CTX)))) { + PyErr_SetString(PyExc_MemoryError, "x509v3_set_conf_lhash"); + return NULL; + } + X509V3_set_conf_lhash(ctx, lhash); + return ctx; +} + +X509_EXTENSION * +#if OPENSSL_VERSION_NUMBER >= 0x10000000L +x509v3_ext_conf(LHASH_OF(CONF_VALUE) *conf, X509V3_CTX *ctx, char *name, char *value) { +#else +x509v3_ext_conf(LHASH *conf, X509V3_CTX *ctx, char *name, char *value) { +#endif + X509_EXTENSION * ext = NULL; + ext = X509V3_EXT_conf(conf, ctx, name, value); + PyMem_Free(ctx); +#if OPENSSL_VERSION_NUMBER >= 0x10000000L + lh_CONF_VALUE_free(conf); +#else + lh_free(conf); +#endif + return ext; +} + +/* X509_EXTENSION_free() might be a macro, didn't find definition. */ +void x509_extension_free(X509_EXTENSION *ext) { + X509_EXTENSION_free(ext); +} + +PyObject *x509_extension_get_name(X509_EXTENSION *ext) { + PyObject * ext_name; + const char * ext_name_str; + ext_name_str = OBJ_nid2sn(OBJ_obj2nid(X509_EXTENSION_get_object(ext))); + if (!ext_name_str) { + PyErr_SetString(_x509_err, ERR_reason_error_string(ERR_get_error())); + return NULL; + } + ext_name = PyString_FromStringAndSize(ext_name_str, strlen(ext_name_str)); + return ext_name; +} + +/* sk_X509_EXTENSION_new_null is a macro. */ +STACK_OF(X509_EXTENSION) *sk_x509_extension_new_null(void) { + return sk_X509_EXTENSION_new_null(); +} + +/* sk_X509_EXTENSION_free() is a macro. */ +void sk_x509_extension_free(STACK_OF(X509_EXTENSION) *stack) { + sk_X509_EXTENSION_free(stack); +} + +/* sk_X509_EXTENSION_push() is a macro. */ +int sk_x509_extension_push(STACK_OF(X509_EXTENSION) *stack, X509_EXTENSION *x509_ext) { + return sk_X509_EXTENSION_push(stack, x509_ext); +} + +/* sk_X509_EXTENSION_pop() is a macro. */ +X509_EXTENSION *sk_x509_extension_pop(STACK_OF(X509_EXTENSION) *stack) { + return sk_X509_EXTENSION_pop(stack); +} + +/* sk_X509_EXTENSION_num() is a macro. */ +int sk_x509_extension_num(STACK_OF(X509_EXTENSION) *stack) { + return sk_X509_EXTENSION_num(stack); +} + +/* sk_X509_EXTENSION_value() is a macro. */ +X509_EXTENSION *sk_x509_extension_value(STACK_OF(X509_EXTENSION) *stack, int i) { + return sk_X509_EXTENSION_value(stack, i); +} + +/* X509_STORE_CTX_get_app_data is a macro. */ +void *x509_store_ctx_get_app_data(X509_STORE_CTX *ctx) { + return X509_STORE_CTX_get_app_data(ctx); +} + +/*#defines for i2d and d2i types, which are typed differently +in openssl-0.9.8 than they are in openssl-0.9.7. This will +be picked up by the C preprocessor, not the SWIG preprocessor. +Used in the wrapping of ASN1_seq_unpack and ASN1_seq_pack functions. +*/ +#if OPENSSL_VERSION_NUMBER >= 0x0090800fL +#define D2ITYPE d2i_of_void * +#define I2DTYPE i2d_of_void * +#else +#define D2ITYPE char *(*)() +#define I2DTYPE int (*)() +#endif + +STACK_OF(X509) * +make_stack_from_der_sequence(PyObject * pyEncodedString){ + STACK_OF(X509) *certs; + Py_ssize_t encoded_string_len; + char *encoded_string; + + encoded_string_len = PyString_Size(pyEncodedString); + if (encoded_string_len > INT_MAX) { + PyErr_SetString(PyExc_ValueError, "object too large"); + return NULL; + } + encoded_string = PyString_AsString(pyEncodedString); + if (!encoded_string) { + return NULL; + } + + certs = ASN1_seq_unpack_X509((unsigned char *)encoded_string, encoded_string_len, d2i_X509, X509_free ); + if (!certs) { + PyErr_SetString(_x509_err, ERR_reason_error_string(ERR_get_error())); + return NULL; + } + + return certs; +} + +PyObject * +get_der_encoding_stack(STACK_OF(X509) *stack){ + PyObject * encodedString; + + unsigned char * encoding; + int len; + + encoding = ASN1_seq_pack_X509(stack, i2d_X509, NULL, &len); + if (!encoding) { + PyErr_SetString(_x509_err, ERR_reason_error_string(ERR_get_error())); + return NULL; + } + encodedString = PyString_FromStringAndSize((const char *)encoding, len); + OPENSSL_free(encoding); + return encodedString; +} + +%} + +/* Free malloc'ed return value for x509_name_oneline */ +%typemap(ret) char * { + if ($1 != NULL) + OPENSSL_free($1); +} +%inline %{ +char *x509_name_oneline(X509_NAME *x) { + return X509_NAME_oneline(x, NULL, 0); +} +%} +%typemap(ret) char *; diff --git a/contrib/README b/contrib/README new file mode 100644 index 0000000..26483f1 --- /dev/null +++ b/contrib/README @@ -0,0 +1,6 @@ +This directory contains contributions by users of M2Crypto. Some of these +may get folded into the main distribution in time. + +Thanks guys! + + diff --git a/contrib/SimpleX509create.README b/contrib/SimpleX509create.README new file mode 100644 index 0000000..a08db85 --- /dev/null +++ b/contrib/SimpleX509create.README @@ -0,0 +1,3 @@ +Contributed by Peter Teniz <peter.teniz@inverisa.net> as a demonstration of +PKI functionality, also contributed by him. + diff --git a/contrib/SimpleX509create.py b/contrib/SimpleX509create.py new file mode 100644 index 0000000..7f5fc67 --- /dev/null +++ b/contrib/SimpleX509create.py @@ -0,0 +1,162 @@ +#!/usr/bin/env python +# +#vim: ts=4 sw=4 nowrap +# + +"""PKI demo by Peter Teniz <peter.teniz@inverisa.net>""" + +import M2Crypto + + +MBSTRING_FLAG = 0x1000 +MBSTRING_ASC = MBSTRING_FLAG | 1 +MBSTRING_BMP = MBSTRING_FLAG | 2 + + +class Cert: + def __init__ ( self ): + self.RsaKey = { 'KeyLength' : 1024, + 'PubExponent' : 0x10001, # -> 65537 + 'keygen_callback' : self.callback + } + + self.KeyPair = None + self.PKey = None + + self.X509Request = None + self.X509Certificate = None + + def callback ( self, *args ): + return 'p' + + + + def CreatePKey ( self ): + self.KeyPair = M2Crypto.RSA.gen_key( self.RsaKey['KeyLength'], self.RsaKey['PubExponent'], self.RsaKey['keygen_callback'] ) + #PubKey = M2Crypto.RSA.new_pub_key( self.KeyPair.pub () ) + + self.KeyPair.save_key( 'KeyPair.pem', cipher='des_ede3_cbc', callback=self.callback ) + + self.PKey = M2Crypto.EVP.PKey ( md='sha1') + self.PKey.assign_rsa ( self.KeyPair ) + + + def CreateX509Request ( self ): + # + # X509 REQUEST + # + + self.X509Request = M2Crypto.X509.Request () + + # + # subject + # + + X509Name = M2Crypto.X509.X509_Name () + + X509Name.add_entry_by_txt ( field='C', type=MBSTRING_ASC, entry='austria', len=-1, loc=-1, set=0 ) # country name + X509Name.add_entry_by_txt ( field='SP', type=MBSTRING_ASC, entry='kernten', len=-1, loc=-1, set=0 ) # state of province name + X509Name.add_entry_by_txt ( field='L', type=MBSTRING_ASC, entry='stgallen', len=-1, loc=-1, set=0 ) # locality name + X509Name.add_entry_by_txt ( field='O', type=MBSTRING_ASC, entry='labor', len=-1, loc=-1, set=0 ) # organization name + X509Name.add_entry_by_txt ( field='OU', type=MBSTRING_ASC, entry='it-department', len=-1, loc=-1, set=0 ) # organizational unit name + X509Name.add_entry_by_txt ( field='CN', type=MBSTRING_ASC, entry='Certificate client', len=-1, loc=-1, set=0 ) # common name + X509Name.add_entry_by_txt ( field='Email', type=MBSTRING_ASC, entry='user@localhost', len=-1, loc=-1, set=0 ) # pkcs9 email address + X509Name.add_entry_by_txt ( field='emailAddress', type=MBSTRING_ASC, entry='user@localhost', len=-1, loc=-1, set=0 ) # pkcs9 email address + + self.X509Request.set_subject_name( X509Name ) + + # + # publickey + # + + self.X509Request.set_pubkey ( pkey=self.PKey ) + self.X509Request.sign ( pkey=self.PKey, md='sha1' ) + #print X509Request.as_text () + + + + + + + def CreateX509Certificate ( self ): + # + # X509 CERTIFICATE + # + + self.X509Certificate = M2Crypto.X509.X509 () + + # + # version + # + + self.X509Certificate.set_version ( 0 ) + + # + # time notBefore + # + + ASN1 = M2Crypto.ASN1.ASN1_UTCTIME () + ASN1.set_time ( 500 ) + self.X509Certificate.set_not_before( ASN1 ) + + # + # time notAfter + # + + ASN1 = M2Crypto.ASN1.ASN1_UTCTIME () + ASN1.set_time ( 500 ) + self.X509Certificate.set_not_after( ASN1 ) + + # + # public key + # + + self.X509Certificate.set_pubkey ( pkey=self.PKey ) + + # + # subject + # + + X509Name = self.X509Request.get_subject () + + #print X509Name.entry_count () + #print X509Name.as_text () + + self.X509Certificate.set_subject_name( X509Name ) + + # + # issuer + # + + X509Name = M2Crypto.X509.X509_Name ( M2Crypto.m2.x509_name_new () ) + + X509Name.add_entry_by_txt ( field='C', type=MBSTRING_ASC, entry='germany', len=-1, loc=-1, set=0 ) # country name + X509Name.add_entry_by_txt ( field='SP', type=MBSTRING_ASC, entry='bavaria', len=-1, loc=-1, set=0 ) # state of province name + X509Name.add_entry_by_txt ( field='L', type=MBSTRING_ASC, entry='munich', len=-1, loc=-1, set=0 ) # locality name + X509Name.add_entry_by_txt ( field='O', type=MBSTRING_ASC, entry='sbs', len=-1, loc=-1, set=0 ) # organization name + X509Name.add_entry_by_txt ( field='OU', type=MBSTRING_ASC, entry='it-department', len=-1, loc=-1, set=0 ) # organizational unit name + X509Name.add_entry_by_txt ( field='CN', type=MBSTRING_ASC, entry='Certificate Authority', len=-1, loc=-1, set=0 ) # common name + X509Name.add_entry_by_txt ( field='Email', type=MBSTRING_ASC, entry='admin@localhost', len=-1, loc=-1, set=0 ) # pkcs9 email address + X509Name.add_entry_by_txt ( field='emailAddress', type=MBSTRING_ASC, entry='admin@localhost', len=-1, loc=-1, set=0 ) # pkcs9 email address + + #print X509Name.entry_count () + #print X509Name.as_text () + + self.X509Certificate.set_issuer_name( X509Name ) + + # + # signing + # + + self.X509Certificate.sign( pkey=self.PKey, md='sha1' ) + print self.X509Certificate.as_text () + + + + + +if __name__ == '__main__': + run = Cert () + run.CreatePKey () + run.CreateX509Request () + run.CreateX509Certificate () diff --git a/contrib/dave.README b/contrib/dave.README new file mode 100644 index 0000000..1a3339a --- /dev/null +++ b/contrib/dave.README @@ -0,0 +1,64 @@ +From dave@pythonapocrypha.com Wed Dec 11 07:57:55 2002 +Date: Tue, 10 Dec 2002 15:05:26 -0800 (PST) +From: Dave Brueck <dave@pythonapocrypha.com> +To: ngps@netmemetic.com +Subject: M2Crypto problem with asynch sockets + +Hi and thanks for M2Crypto - great stuff! + +I wrote an asynchronous socket layer and decided to use M2Crypto to add +SSL support to it. Unfortunately, I've found a small problem in +_m2crypto_wrap.c - hopefully I'm just not understanding something. + +The ssl_connect, ssl_read_nbio, etc. calls don't differentiate between +SSL_ERROR_WANT_WRITE and SSL_ERROR_WANT_READ when a non-blocking call +couldn't finish. But without this information, I don't know whether the +socket needs to do more reading or more writing before a subsequent +attempt will work without blocking. The demo applications (e.g. +echod-async.py) don't seem to care about this but they get around it by +simply trying the operation over and over again, which I can't do for +performance reasons. + +Am I missing something? I thought about just calling SSL_get_error when +the above calls return None (indicating WANT_READ or WANT_WRITE), but by +then the error has already been removed from the SSL error queue. + +Any help or suggestions would be appreciated. I'd be happy to submit a +patch fixing those calls, but by not returning None they would break +existing code. + +Thanks again for M2Crypto though! + +-Dave + + +From dave@pythonapocrypha.com Fri Dec 13 00:46:39 2002 +Date: Thu, 12 Dec 2002 09:52:08 -0800 (PST) +From: Dave Brueck <dave@pythonapocrypha.com> +To: ngps@netmemetic.com +Subject: Re: M2Crypto problem with asynch sockets + +Hello again, + +Here is a patch to M2Crypto's _ssl.i that illustrates the fix I had in +mind in my previous message. You might not want to use it as is since it +changes the error semantics of the affected functions (they now raise an +exception that contains the SSL_WANT_READ or SSL_WANT_WRITE flag instead +of returning None or whatever), but if you tell me how you'd like it +instead then I'd be happy to fix the patch and send it back to you. + +Just to refresh your memory, this patch fixes the problem where a +non-blocking call to accept/connect/etc results in an SSL_NEED_READ/WRITE; +currently there's no way for the caller to know _which_ of the two +occurred and so it must try again once the socket has become readable OR +writeable, instead of waiting specifically for one or the other. For many +people this won't matter because their performance requirements are low +enough that trying the ssl_accept/etc call again prematurely won't hurt +too much, but for servers with lots of connections or high throughput it's +much more critical to wait until you know the SSL call has a better chance +of success. + +Thanks! +-Dave Brueck + + diff --git a/contrib/dave.patch b/contrib/dave.patch new file mode 100644 index 0000000..6e41073 --- /dev/null +++ b/contrib/dave.patch @@ -0,0 +1,180 @@ +--- _ssl.i.orig Tue Sep 18 06:06:34 2001 ++++ _ssl.i Thu Dec 12 08:57:39 2002 +@@ -238,9 +238,18 @@ + return ret; + } + ++void SetWantRWError(int which) { ++ PyObject *o = Py_BuildValue("(is)", which, ++ +ERR_reason_error_string(ERR_get_error())); ++ if (o != NULL) { ++ PyErr_SetObject(_ssl_err, o); ++ Py_DECREF(o); ++ } ++} ++ + PyObject *ssl_accept(SSL *ssl) { + PyObject *obj; +- int r, err; ++ int r, err, which; + PyThreadState *_save; + + if (thread_mode) { +@@ -252,14 +261,15 @@ + _save = (PyThreadState *)SSL_get_app_data(ssl); + PyEval_RestoreThread(_save); + } +- switch (SSL_get_error(ssl, r)) { ++ switch ((which = SSL_get_error(ssl, r))) { + case SSL_ERROR_NONE: + case SSL_ERROR_ZERO_RETURN: + obj = PyInt_FromLong((long)1); + break; + case SSL_ERROR_WANT_WRITE: + case SSL_ERROR_WANT_READ: +- obj = PyInt_FromLong((long)0); ++ SetWantRWError(which); ++ obj = NULL; + break; + case SSL_ERROR_SSL: + PyErr_SetString(_ssl_err, +ERR_reason_error_string(ERR_get_error())); +@@ -281,7 +291,7 @@ + + PyObject *ssl_connect(SSL *ssl) { + PyObject *obj; +- int r, err; ++ int r, err, which; + PyThreadState *_save; + + if (thread_mode) { +@@ -293,14 +303,15 @@ + _save = (PyThreadState *)SSL_get_app_data(ssl); + PyEval_RestoreThread(_save); + } +- switch (SSL_get_error(ssl, r)) { ++ switch ((which = SSL_get_error(ssl, r))) { + case SSL_ERROR_NONE: + case SSL_ERROR_ZERO_RETURN: + obj = PyInt_FromLong((long)1); + break; + case SSL_ERROR_WANT_WRITE: + case SSL_ERROR_WANT_READ: +- obj = PyInt_FromLong((long)0); ++ SetWantRWError(which); ++ obj = NULL; + break; + case SSL_ERROR_SSL: + PyErr_SetString(_ssl_err, +ERR_reason_error_string(ERR_get_error())); +@@ -327,7 +338,7 @@ + PyObject *ssl_read(SSL *ssl, int num) { + PyObject *obj; + void *buf; +- int r, err; ++ int r, err, which; + PyThreadState *_save; + + if (!(buf = PyMem_Malloc(num))) { +@@ -343,7 +354,7 @@ + _save = (PyThreadState *)SSL_get_app_data(ssl); + PyEval_RestoreThread(_save); + } +- switch (SSL_get_error(ssl, r)) { ++ switch ((which = SSL_get_error(ssl, r))) { + case SSL_ERROR_NONE: + case SSL_ERROR_ZERO_RETURN: + buf = PyMem_Realloc(buf, r); +@@ -352,8 +363,8 @@ + case SSL_ERROR_WANT_WRITE: + case SSL_ERROR_WANT_READ: + case SSL_ERROR_WANT_X509_LOOKUP: +- Py_INCREF(Py_None); +- obj = Py_None; ++ SetWantRWError(which); ++ obj = NULL; + break; + case SSL_ERROR_SSL: + PyErr_SetString(_ssl_err, +ERR_reason_error_string(ERR_get_error())); +@@ -377,14 +388,14 @@ + PyObject *ssl_read_nbio(SSL *ssl, int num) { + PyObject *obj; + void *buf; +- int r, err; ++ int r, err, which; + + if (!(buf = PyMem_Malloc(num))) { + PyErr_SetString(PyExc_MemoryError, "ssl_read"); + return NULL; + } + r = SSL_read(ssl, buf, num); +- switch (SSL_get_error(ssl, r)) { ++ switch ((which = SSL_get_error(ssl, r))) { + case SSL_ERROR_NONE: + case SSL_ERROR_ZERO_RETURN: + buf = PyMem_Realloc(buf, r); +@@ -392,6 +403,9 @@ + break; + case SSL_ERROR_WANT_WRITE: + case SSL_ERROR_WANT_READ: ++ SetWantRWError(which); ++ obj = NULL; ++ break; + case SSL_ERROR_WANT_X509_LOOKUP: + Py_INCREF(Py_None); + obj = Py_None; +@@ -417,7 +431,7 @@ + + int ssl_write(SSL *ssl, PyObject *blob) { + const void *buf; +- int len, r, err; ++ int len, r, err, which; + PyThreadState *_save; + + #if PYTHON_API_VERSION >= 1009 +@@ -440,12 +454,14 @@ + _save = (PyThreadState *)SSL_get_app_data(ssl); + PyEval_RestoreThread(_save); + } +- switch (SSL_get_error(ssl, r)) { ++ switch ((which = SSL_get_error(ssl, r))) { + case SSL_ERROR_NONE: + case SSL_ERROR_ZERO_RETURN: + return r; + case SSL_ERROR_WANT_WRITE: + case SSL_ERROR_WANT_READ: ++ SetWantRWError(which); ++ return -1; + case SSL_ERROR_WANT_X509_LOOKUP: + return -1; + case SSL_ERROR_SSL: +@@ -466,7 +482,7 @@ + + int ssl_write_nbio(SSL *ssl, PyObject *blob) { + const void *buf; +- int len, r, err; ++ int len, r, err, which; + + #if PYTHON_API_VERSION >= 1009 + if (PyObject_AsReadBuffer(blob, &buf, &len) == -1) +@@ -480,12 +496,14 @@ + buf = (const void *)PyString_AsString(blob); + #endif + r = SSL_write(ssl, buf, len); +- switch (SSL_get_error(ssl, r)) { ++ switch ((which = SSL_get_error(ssl, r))) { + case SSL_ERROR_NONE: + case SSL_ERROR_ZERO_RETURN: + return r; + case SSL_ERROR_WANT_WRITE: + case SSL_ERROR_WANT_READ: ++ SetWantRWError(which); ++ return -1; + case SSL_ERROR_WANT_X509_LOOKUP: + return -1; + case SSL_ERROR_SSL: + + + diff --git a/contrib/dispatcher.README b/contrib/dispatcher.README new file mode 100644 index 0000000..3049a5e --- /dev/null +++ b/contrib/dispatcher.README @@ -0,0 +1,30 @@ +Date: Thu, 31 May 2001 17:11:45 +0400 (MSD) +From: Ilya Etingof <ilya@glas.net> +To: ngps@post1.com +Cc: Ilya Etingof <ilya@glas.net> +Subject: Another kind of non-blocking SSL dispatcher + +--1922505501-409592217-991314705=:1995 +Content-Type: TEXT/PLAIN; charset=US-ASCII + + +Hi, + +Thanks for writing M2Crypto! + +I've been trying to use the ssl_dispatcher.py though I felt like the +bundled version is not absolutely non-blocking. Precisely, it looks +like the Connection.connect() method does not handle the case when +socket.connect() returns the WOULDBLOCK error. Another suspicious thing +is that there seems to be no SSL "want read" and "want write" error +return codes of SSL read and write functions. + +The attached [quick and dirty] code hopefully fixes these two +problems. Please, let me know if I'm missing some important clue about all +this. + +Thanks, +ilya + +--1922505501-409592217-991314705=:1995 + diff --git a/contrib/dispatcher.py b/contrib/dispatcher.py new file mode 100644 index 0000000..6e7302d --- /dev/null +++ b/contrib/dispatcher.py @@ -0,0 +1,191 @@ +#!/usr/local/bin/python -O
+"""
+ Implements a [hopefully] non-blocking SSL dispatcher on top of
+ M2Crypto package.
+
+ Written by Ilya Etingof <ilya@glas.net>, 05/2001
+"""
+import asyncore, socket
+
+# M2Crypto
+from M2Crypto import SSL
+
+class _nb_connection (SSL.Connection):
+ """Functional equivalent of SSL.Connection class. Facilitates
+ possibly delayed socket.connect() and socket.accept()
+ termination.
+ """
+ def __init__ (self, ctx, sock):
+ SSL.Connection.__init__ (self, ctx, sock)
+
+ def connect(self, addr):
+ self._setup_ssl(addr)
+ return self._check_ssl_return(SSL.m2.ssl_connect(self.ssl))
+
+ def accept(self, addr):
+ self._setup_ssl(addr)
+ self.accept_ssl()
+
+class dispatcher(asyncore.dispatcher_with_send):
+ """A non-blocking SSL dispatcher that mimics the
+ asyncode.dispatcher API.
+ """
+ def __init__ (self, cert, key, sock=None, serving=None):
+ asyncore.dispatcher_with_send.__init__ (self)
+
+ self.__serving = serving
+
+ # XXX
+ if sock:
+ if self.__serving:
+ self.set_socket(sock)
+ else:
+ self.create_socket (socket.AF_INET, socket.SOCK_STREAM)
+
+ self.ctx = SSL.Context('sslv23')
+ self.ctx.set_verify(SSL.verify_none, 10)
+ self.ctx.load_cert(cert, key)
+ self.ctx.set_info_callback()
+
+ self.ssl = _nb_connection(self.ctx, self.socket)
+
+ self.__output = ''
+ self.__want_write = 1
+
+ #
+ # The following are asyncore overloaded methods
+ #
+
+ def handle_connect (self):
+ """Initiate SSL connection negotiation
+ """
+ if self.__serving:
+ self.ssl.accept (self.addr)
+
+ self.peer = self.ssl.get_peer_cert()
+
+ self.handle_ssl_accept()
+
+ else:
+ self.ssl.connect (self.addr)
+
+ self.handle_ssl_connect()
+
+ def handle_read(self):
+ """Read user and/or SSL protocol data from SSL connection
+ """
+ ret = self.ssl._read_nbio()
+
+ if ret:
+ self.handle_ssl_read(ret)
+ else:
+ # Assume write is wanted
+ self.__want_write = 1
+
+ def handle_write(self):
+ """Write pending user and/or SSL protocol data down to SSL
+ connection
+ """
+ self.__want_write = 0
+
+ ret = self.ssl._write_nbio(self.__output)
+
+ if ret < 0:
+ try:
+ err = SSL.m2.ssl_get_error(self.ssl.ssl, ret)
+
+ except SSL.SSLError:
+ return
+
+ if err == SSL.m2.ssl_error_want_write:
+ self.__want_write = 1
+ else:
+ self.__output = self.__output[ret:]
+
+ def writable (self):
+ """Indicate that write is desired if here're some
+ user and/or SSL protocol data.
+ """
+ if self.__output or self.__want_write:
+ return 1
+
+ return self.ssl_writable()
+
+ def handle_close (self):
+ """Shutdown SSL connection.
+ """
+ self.ssl = None
+
+ self.ctx = None
+ self.close ()
+
+ self.handle_ssl_close()
+
+ def handle_error (self, *info):
+ """A trap for asyncore errors
+ """
+ self.handle_ssl_error(info)
+
+ #
+ # The following are ssl.dispatcher API
+ #
+
+ def ssl_connect(self, server):
+ """Initiate SSL connection
+ """
+ self.connect(server)
+
+ def ssl_write(self, data):
+ """Write data to SSL connection
+ """
+ self.__output = self.__output + data
+
+ def ssl_close(self):
+ """Close SSL connection
+ """
+ self.handle_close()
+
+ def handle_ssl_connect(self):
+ """Invoked on SSL connection establishment (whilst
+ in client mode)
+ """
+ print 'Unhandled handle_ssl_connect()'
+
+ def handle_ssl_accept(self):
+ """Invoked on SSL connection establishment (whilst
+ in server mode)
+ """
+ print 'Unhandled handle_ssl_accept()'
+
+ def handle_ssl_read(self, data):
+ """Invoked on new data arrival to SSL connection
+ """
+ print 'Unhandled handle_ssl_read event'
+
+ def handle_ssl_close(self):
+ """Invoked on SSL connection termination
+ """
+ pass
+
+ def ssl_writable(self):
+ """Invoked prior to every select() call
+ """
+ return 0
+
+if __name__=='__main__':
+ """Give it a test run
+ """
+ class client(dispatcher):
+ """SSL client class
+ """
+ def __init__ (self, cert, key):
+ dispatcher.__init__(self, cert, key)
+
+ def handle_ssl_read(self, data):
+ print data
+ self.ssl_write('test write')
+
+ ssl = client('test.cert', 'test.key')
+ ssl.ssl_connect(('localhost', 7777))
+
+ asyncore.loop()
diff --git a/contrib/isaac.README b/contrib/isaac.README new file mode 100644 index 0000000..b47932e --- /dev/null +++ b/contrib/isaac.README @@ -0,0 +1,18 @@ +This is Isaac Salzberg's application of Mihai Ibanescu's patch +(available on SF) that allows HTTPS tunneling through an +authenticating proxy. + +This one's a double whammy: it works with IIS through the +authenticating proxy, whereas the one on SF, which uses Python's +built-in SSL, doesn't. + +This code is not folded into the main distribution because: + +1. Apparently Mihai is still working on it. +2. Mihai uses Python's built-in SSL. Isaac patched it to use + M2Crypto.SSL. The stuff is essentially #ifdef'ed code. +3. I don't have an authenticating proxy nor an IIS server to test + against, so I can't clean up the code. Volunteers welcome. ;-) + +Thanks Isaac. + diff --git a/contrib/isaac.httpslib.py b/contrib/isaac.httpslib.py new file mode 100644 index 0000000..ae9d23d --- /dev/null +++ b/contrib/isaac.httpslib.py @@ -0,0 +1,269 @@ +"""M2Crypto support for Python 1.5.2 and Python 2.x's httplib. + +Copyright (c) 1999-2002 Ng Pheng Siong. All rights reserved.""" + +import string, sys +from httplib import * +import SSL + +if sys.version[0] == '2': + + if sys.version[:3] in ['2.1', '2.2']: + # In 2.1 and above, httplib exports "HTTP" only. + from httplib import HTTPConnection, HTTPS_PORT + # ISS Added: + from httplib import HTTPResponse,FakeSocket + + class HTTPSConnection(HTTPConnection): + + """ + This class allows communication via SSL using M2Crypto. + """ + + default_port = HTTPS_PORT + + def __init__(self, host, port=None, **ssl): + keys = ssl.keys() + try: + keys.remove('key_file') + except ValueError: + pass + try: + keys.remove('cert_file') + except ValueError: + pass + try: + keys.remove('ssl_context') + except ValueError: + pass + if keys: + raise IllegalKeywordArgument() + try: + self.ssl_ctx = ssl['ssl_context'] + assert isinstance(self.ssl_ctx, SSL.Context) + except KeyError: + self.ssl_ctx = SSL.Context('sslv23') + HTTPConnection.__init__(self, host, port) + + def connect(self): + self.sock = SSL.Connection(self.ssl_ctx) + self.sock.connect((self.host, self.port)) + + def close(self): + # This kludges around line 545 of httplib.py, + # which closes the connection in this object; + # the connection remains open in the response + # object. + # + # M2Crypto doesn't close-here-keep-open-there, + # so, in effect, we don't close until the whole + # business is over and gc kicks in. + # + # Long-running callers beware leakage. + # + # 05-Jan-2002: This module works with Python 2.2, + # but I've not investigated if the above conditions + # remain. + pass + + + class HTTPS(HTTP): + + _connection_class = HTTPSConnection + + def __init__(self, host='', port=None, **ssl): + HTTP.__init__(self, host, port) + try: + self.ssl_ctx = ssl['ssl_context'] + except KeyError: + self.ssl_ctx = SSL.Context('sslv23') + + +elif sys.version[:3] == '1.5': + + class HTTPS(HTTP): + + def __init__(self, ssl_context, host='', port=None): + assert isinstance(ssl_context, SSL.Context) + self.debuglevel=0 + self.file=None + self.ssl_ctx=ssl_context + if host: + self.connect(host, port) + + def connect(self, host, port=None): + # Cribbed from httplib.HTTP. + if not port: + i = string.find(host, ':') + if i >= 0: + host, port = host[:i], host[i+1:] + try: port = string.atoi(port) + except string.atoi_error: + raise socket.error, "nonnumeric port" + if not port: port = HTTPS_PORT + self.sock = SSL.Connection(self.ssl_ctx) + if self.debuglevel > 0: print 'connect:', (host, port) + self.sock.connect((host, port)) + +# ISS Added. +# From here, starts the proxy patch +class HTTPProxyConnection(HTTPConnection): + """ + This class provides HTTP access through (authenticated) proxies. + + Example: + If the HTTP proxy address is proxy.your.org:8080, an authenticated proxy + (one which requires a username/password combination in order to serve + requests), one can fetch HTTP documents from 'www.webserver.net', port 81: + + conn = HTTPProxyConnection('proxy.your.org:8080', 'www.webserver.net', + port=81, username='username', password='password') + conn.connect() + conn.request("HEAD", "/index.html", headers={'X-Custom-Header-1' : 'Value-1'}) + resp = conn.getresponse() + ... + + """ + def __init__(self, proxy, host, port=None, username=None, password=None): + # The connection goes through the proxy + HTTPConnection.__init__(self, proxy) + # save the proxy connection settings + self.__proxy, self.__proxy_port = self.host, self.port + # self.host and self.port will point to the real host + self._set_hostport(host, port) + # save the host and port + self._host, self._port = self.host, self.port + # Authenticated proxies support + self.__username = username + self.__password = password + + def connect(self): + """Connect to the host and port specified in __init__ (through a + proxy).""" + # We are connecting to the proxy, so use the proxy settings + self._set_hostport(self.__proxy, self.__proxy_port) + HTTPConnection.connect(self) + # Restore the real host and port + self._set_hostport(self._host, self._port) + + def putrequest(self, method, url): + """Send a request to the server. + + `method' specifies an HTTP request method, e.g. 'GET'. + `url' specifies the object being requested, e.g. '/index.html'. + """ + # The URL has to include the real host + hostname = self._host + if self._port != self.default_port: + hostname = hostname + ':' + str(self._port) + newurl = "http://%s%s" % (hostname, url) + # Piggyback on the parent class + HTTPConnection.putrequest(self, method, newurl) + # Add proxy-specific headers + self._add_auth_proxy_header() + + def _add_auth_proxy_header(self): + """Adds an HTTP header for authenticated proxies + """ + if not self.__username: + # No username, so assume not an authenticated proxy + return + # Authenticated proxy + import base64 + userpass = "%s:%s" % (self.__username, self.__password) + enc_userpass = string.strip(base64.encodestring(userpass)) + self.putheader("Proxy-Authorization", "Basic %s" % enc_userpass) + +class HTTPSProxyResponse(HTTPResponse): + """ + Replacement class for HTTPResponse + Proxy responses (made through SSL) have to keep the connection open + after the initial request, since the connection is tunneled to the SSL + host with the CONNECT method. + """ + def begin(self): + HTTPResponse.begin(self) + self.will_close = 0 + +class HTTPSProxyConnection(HTTPProxyConnection): + """This class provides HTTP access through (authenticated) proxies. + + Example: + If the HTTP proxy address is proxy.your.org:8080, an authenticated proxy + (one which requires a username/password combination in order to serve + requests), one can fetch HTTP documents from 'www.webserver.net', port 81: + + conn = HTTPProxyConnection('proxy.your.org:8080', 'www.webserver.net', + port=81, username='username', password='password') + conn.connect() + conn.request("HEAD", "/index.html", headers={'X-Custom-Header-1' : 'Value-1'}) + resp = conn.getresponse() + ... + + To avoid dealing with multiple inheritance, this class only inherits from + HTTPProxyConnection. + """ + default_port = HTTPSConnection.default_port + + def __init__(self, proxy, host, port=None, username=None, password=None, **x509): + for key in x509.keys(): + if key not in ['cert_file', 'key_file','ssl_context']: + raise IllegalKeywordArgument() + self.key_file = x509.get('key_file') + self.cert_file = x509.get('cert_file') + #ISS Added + self.ssl_ctx = x509.get('ssl_context') + # Piggybacking on HTTPProxyConnection + HTTPProxyConnection.__init__(self, proxy, host, port, username, password) + + def connect(self): + """Connect (using SSL) to the host and port specified in __init__ + (through a proxy).""" + import socket + # Set the connection with the proxy + HTTPProxyConnection.connect(self) + # Use the stock HTTPConnection putrequest + host = "%s:%s" % (self._host, self._port) + HTTPConnection.putrequest(self, "CONNECT", host) + # Add proxy-specific stuff + self._add_auth_proxy_header() + # And send the request + HTTPConnection.endheaders(self) + # Save the response class + response_class = self.response_class + # And replace the response class with our own one, which does not + # close the connection + self.response_class = HTTPSProxyResponse + response = HTTPConnection.getresponse(self) + # Restore the response class + self.response_class = response_class + # Close the response object manually + response.close() + if response.status != 200: + # Close the connection manually + self.close() + # XXX Find the appropriate error code + raise socket.error(1001, response.status, response.value) + + # NgPS: I haven't read the code recently, but I think it is + # reasonable to assume that self.sock is a connected TCP socket at + # this point. + + # Use the real stuff. ;-) + if self.ssl_ctx and isinstance(self.ssl_ctx, SSL.Context): + self.sock = SSL.Connection(self.ssl_ctx) + self.sock.connect((self.host, self.port)) + else: + # Fake the socket + ssl = socket.ssl(self.sock, self.key_file, self.cert_file) + self.sock = FakeSocket(self.sock, ssl) + if self.debuglevel > 0: print 'socket type:', self.sock + + def putrequest(self, method, url): + """Send a request to the server. + + `method' specifies an HTTP request method, e.g. 'GET'. + `url' specifies the object being requested, e.g. '/index.html'. + """ + # bypass the parent class's putrequest: use the grandparent's one :-) + return HTTPConnection.putrequest(self, method, url) diff --git a/contrib/m2crypto.spec b/contrib/m2crypto.spec new file mode 100644 index 0000000..f80cddb --- /dev/null +++ b/contrib/m2crypto.spec @@ -0,0 +1,46 @@ +%define name m2crypto +%define version 0.06 +%define snap snap5 +%define release %{snap}.1 +%define prefix %{_prefix} + +Summary: Python crypto library +Name: %{name} +Version: %{version} +Release: %{release} +Copyright: tummy.com, ltd. +Group: Applications/Crypto +Source: %{name}-%{version}-%{snap}.zip +Packager: Sean Reifschneider <jafo-rpms@tummy.com> +BuildRoot: /var/tmp/%{name}-root +Requires: openssl >= 0.9.6a +Patch0: m2crypto-makefile.patch +BuildPrereq: openssl-devel >= 0.9.6a +BuildPrereq: swig >= 1.1p5 + +%description +M2Crypto makes available to the Python programmer the following: + + RSA, DH, DSA, HMACs, message digests, symmetric ciphers. + SSL functionality to implement clients and servers. + HTTPS extensions to Python's httplib, urllib, and the eff-bot's xmlrpclib. + S/MIME v2. + +%prep +%setup -n %{name}-%{version}-%{snap} +%patch0 -p1 +%build +( cd swig; make -f Makefile.py1 ) + +%install +[ -n "$RPM_BUILD_ROOT" -a "$RPM_BUILD_ROOT" != / ] && rm -rf "$RPM_BUILD_ROOT" +mkdir -p "$RPM_BUILD_ROOT"/usr/lib/python1.5/site-packages +cp -a M2Crypto "$RPM_BUILD_ROOT"/usr/lib/python1.5/site-packages + +%clean +[ -n "$RPM_BUILD_ROOT" -a "$RPM_BUILD_ROOT" != / ] && rm -rf "$RPM_BUILD_ROOT" + +%files +%defattr(755,root,root) +%doc BUGS CHANGES INSTALL LICENCE README STORIES doc demo tests patches +/usr/lib/python1.5/site-packages diff --git a/contrib/smimeplus.README b/contrib/smimeplus.README new file mode 100644 index 0000000..2125b9f --- /dev/null +++ b/contrib/smimeplus.README @@ -0,0 +1,3 @@ +Contributed by Bernard Yue: A high level smime interface. "It works +for Python 2.2 and above, and requires the email module from Python +2.2.3 to work on Python 2.1. diff --git a/contrib/smimeplus.py b/contrib/smimeplus.py new file mode 100644 index 0000000..5a9c22d --- /dev/null +++ b/contrib/smimeplus.py @@ -0,0 +1,187 @@ +import sys, os, tempfile +import UserDict +from email import Message +import M2Crypto + +if not (sys.version_info[0] >= 2 and sys.version_info[1] >= 2): + class object: + pass + True=1 + False=0 + + +class smimeplus(object): + def __init__(self, cert, privkey, passphrase, cacert, randfile=None): + self.cipher = 'des_ede3_cbc' # XXX make it configable?? + self.setsender(cert, privkey, passphrase) + self.setcacert(cacert) + self.randfile = randfile + self.__loadrand() + + def __passcallback(self, v): + """private key passphrase callback function""" + return self.passphrase + + def __loadrand(self): + """Load random number file""" + if self.randfile: + M2Crypto.Rand.load_file(self.randfile, -1) + + def __saverand(self): + """Save random number file""" + if self.randfile: + M2Crypto.Rand.save_file(self.randfile) + + def __gettext(self, msg): + """Return a string representation of 'msg'""" + _data = '' + if isinstance(msg, Message.Message): + for _p in msg.walk(): + _data = _data + _p.as_string() + else: + _data = str(msg) + return _data + + def __pack(self, msg): + """Convert 'msg' to string and put it into an memory buffer for + openssl operation""" + return M2Crypto.BIO.MemoryBuffer(self.__gettext(msg)) + + def setsender(self, cert=None, privkey=None, passphrase=None): + if cert: + self.cert = cert + if privkey: + self.key = privkey + if passphrase: + self.passphrase = passphrase + + def setcacert(self, cacert): + self.cacert = cacert + + def sign(self, msg): + """Sign a message""" + _sender = M2Crypto.SMIME.SMIME() + _sender.load_key_bio(self.__pack(self.key), self.__pack(self.cert), + callback=self.__passcallback) + + _signed = _sender.sign(self.__pack(msg)) + + _out = self.__pack(None) + _sender.write(_out, _signed, self.__pack(msg)) + return _out.read() + + def verify(self, smsg, scert): + """Verify to see if 'smsg' was signed by 'scert', and scert was + issued by cacert of this object. Return message signed if success, + None otherwise""" + # Load signer's cert. + _x509 = M2Crypto.X509.load_cert_bio(self.__pack(scert)) + _stack = M2Crypto.X509.X509_Stack() + _stack.push(_x509) + + # Load CA cert. + _tmpfile = persistdata(self.cacert) + _store = M2Crypto.X509.X509_Store() + _store.load_info(_tmpfile) + os.remove(_tmpfile) + + # prepare SMIME object + _sender = M2Crypto.SMIME.SMIME() + _sender.set_x509_stack(_stack) + _sender.set_x509_store(_store) + + # Load signed message, verify it, and return result + _p7, _data = M2Crypto.SMIME.smime_load_pkcs7_bio(self.__pack(smsg)) + try: + return _sender.verify(_p7, flags=M2Crypto.SMIME.PKCS7_SIGNED) + except M2Crypto.SMIME.SMIME_Error, _msg: + return None + + def encrypt(self, rcert, msg): + # Instantiate an SMIME object. + _sender = M2Crypto.SMIME.SMIME() + + # Load target cert to encrypt to. + _x509 = M2Crypto.X509.load_cert_bio(self.__pack(rcert)) + _stack = M2Crypto.X509.X509_Stack() + _stack.push(_x509) + _sender.set_x509_stack(_stack) + + _sender.set_cipher(M2Crypto.SMIME.Cipher(self.cipher)) + + # Encrypt the buffer. + _buf = self.__pack(self.__gettext(msg)) + _p7 = _sender.encrypt(_buf) + + # Output p7 in mail-friendly format. + _out = self.__pack('') + _sender.write(_out, _p7) + + # Save the PRNG's state. + self.__saverand() + + return _out.read() + + def decrypt(self, emsg): + """decrypt 'msg'. Return decrypt message if success, None + otherwise""" + # Load private key and cert. + _sender = M2Crypto.SMIME.SMIME() + _sender.load_key_bio(self.__pack(self.key), self.__pack(self.cert), + callback=self.__passcallback) + + # Load the encrypted data. + _p7, _data = M2Crypto.SMIME.smime_load_pkcs7_bio(self.__pack(emsg)) + + # Decrypt p7. + try: + return _sender.decrypt(_p7) + except M2Crypto.SMIME.SMIME_Error, _msg: + return None + + def addHeader(self, rcert, content, subject=''): + """Add To, From, Subject Header to 'content'""" + _scert = M2Crypto.X509.load_cert_bio(self.__pack(self.cert)) + _scertsubj = X509_Subject(str(_scert.get_subject())) + _rcert = M2Crypto.X509.load_cert_bio(self.__pack(rcert)) + _rcertsubj = X509_Subject(str(_rcert.get_subject())) + + _out = 'From: "%(CN)s" <%(emailAddress)s>\n' % _scertsubj + _out = _out + 'To: "%(CN)s" <%(emailAddress)s>\n' % _rcertsubj + _out = _out + 'Subject: %s\n' % subject + _out = _out + content + + return _out + + +class X509_Subject(UserDict.UserDict): + # This class needed to be rewritten or merge with X509_Name + def __init__(self, substr): + UserDict.UserDict.__init__(self) + try: + _data = substr.strip().split('/') + except AttributeError, _msg: + pass + else: + for _i in _data: + try: + _k, _v = _i.split('=') + self[_k] = _v + except ValueError, _msg: + pass + + +def persistdata(data, file=None, isbinary=False): + if not file: + file = tempfile.mktemp() + if isbinary: + _flag = 'wb' + else: + _flag = 'w' + + _fh = open(file, _flag) + _fh.write(data) + _fh.close() + return file + + diff --git a/demo/CipherSaber/CipherSaber.py b/demo/CipherSaber/CipherSaber.py new file mode 100644 index 0000000..81bd4c6 --- /dev/null +++ b/demo/CipherSaber/CipherSaber.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python + +"""CipherSaber, http://ciphersaber.gurus.com. + +Copyright (c) 1999-2003 Ng Pheng Siong. All rights reserved.""" + +# XXX getopt handling has bugs. + +from M2Crypto import RC4, Rand +import getopt, getpass, sys + +class argerr(Exception): pass + +cmd = -1 +inf = sys.stdin +outf = sys.stdout + +optlist, optarg = getopt.getopt(sys.argv[1:], 'dei:o:') +for opt in optlist: + if '-d' in opt: + cmd = cmd + 1 + elif '-e' in opt: + cmd = cmd + 2 + elif '-i' in opt: + i = opt[1] + if i == '-': + inf = sys.stdin + else: + inf = open(i, 'rb') + elif '-o' in opt: + o = opt[1] + if o == '-': + outf = sys.stdout + else: + outf = open(o, 'wb') +if cmd < 0: + raise argerr, "either -d or -e" +if cmd > 1: + raise argerr, "either -d or -e, not both" + +if cmd == 0: + iv = inf.read(10) + pp = getpass.getpass('Enter decryption passphrase: ') +else: + iv = Rand.rand_bytes(10) + outf.write(iv) + pp = getpass.getpass('Enter encryption passphrase: ') + pp2 = getpass.getpass('Enter passphrase again: ') + if pp != pp2: + raise SystemExit, 'passphrase mismatch, I\'m outta here...' + +ci = RC4.RC4(pp + iv) +del pp, iv + +while 1: + buf = inf.read() + if not buf: + break + outf.write(ci.update(buf)) +outf.write(ci.final()) + + diff --git a/demo/CipherSaber/cstest1.cs1 b/demo/CipherSaber/cstest1.cs1 new file mode 100644 index 0000000..a9794b6 --- /dev/null +++ b/demo/CipherSaber/cstest1.cs1 @@ -0,0 +1 @@ +om«óªg0í¶wÊtàÐ縅CV»Hã|Ûïçó¨OO_³ý
\ No newline at end of file diff --git a/demo/Zope/ZServer/HTTPS_Server.py b/demo/Zope/ZServer/HTTPS_Server.py new file mode 100644 index 0000000..06a2668 --- /dev/null +++ b/demo/Zope/ZServer/HTTPS_Server.py @@ -0,0 +1,187 @@ +############################################################################## +# +# Copyright (c) 2000-2004, Ng Pheng Siong. All Rights Reserved. +# This file is derived from Zope's ZServer/HTTPServer.py. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE +# +############################################################################## + +""" +Medusa HTTPS server for Zope + +changes from Medusa's http_server: + + Request Threads -- Requests are processed by threads from a thread + pool. + + Output Handling -- Output is pushed directly into the producer + fifo by the request-handling thread. The HTTP server does not do + any post-processing such as chunking. + + Pipelineable -- This is needed for protocols such as HTTP/1.1 in + which mutiple requests come in on the same channel, before + responses are sent back. When requests are pipelined, the client + doesn't wait for the response before sending another request. The + server must ensure that responses are sent back in the same order + as requests are received. + + +changes from Zope's HTTP server: + + Well, this is a *HTTPS* server :) + + X.509 certificate-based authentication -- When this is in force, + zhttps_handler, a subclass of zhttp_handler, is installed. The + https server is configured to request an X.509 certificate from + the client. When the request reaches zhttps_handler, it sets + REMOTE_USER to the client's subject distinguished name (DN) from + the certificate. Zope's REMOTE_USER machinery takes care of the + rest, e.g., in conjunction with the RemoteUserFolder product. + +""" + +import sys, time, types + +from PubCore import handle +from medusa import asyncore +from ZServer import CONNECTION_LIMIT, ZOPE_VERSION +from HTTPServer import zhttp_handler +from zLOG import register_subsystem + +from M2Crypto import SSL, version +from medusa.https_server import https_server, https_channel +from medusa.asyncore import dispatcher + + +ZSERVER_SSL_VERSION=version + +register_subsystem('ZServer HTTPS_Server') + + +class zhttps0_handler(zhttp_handler): + "zhttps0 handler - sets SSL request headers a la mod_ssl" + + def __init__ (self, module, uri_base=None, env=None): + zhttp_handler.__init__(self, module, uri_base, env) + + def get_environment(self, request): + env = zhttp_handler.get_environment(self, request) + env['SSL_CIPHER'] = request.channel.get_cipher() + return env + + +class zhttps_handler(zhttps0_handler): + "zhttps handler - sets REMOTE_USER to user's X.509 certificate Subject DN" + + def __init__ (self, module, uri_base=None, env=None): + zhttps0_handler.__init__(self, module, uri_base, env) + + def get_environment(self, request): + env = zhttps0_handler.get_environment(self, request) + peer = request.channel.get_peer_cert() + if peer is not None: + env['REMOTE_USER'] = str(peer.get_subject()) + return env + + +class zhttps_channel(https_channel): + "https channel" + + closed=0 + zombie_timeout=100*60 # 100 minutes + + def __init__(self, server, conn, addr): + https_channel.__init__(self, server, conn, addr) + self.queue=[] + self.working=0 + self.peer_found=0 + + def push(self, producer, send=1): + # this is thread-safe when send is false + # note, that strings are not wrapped in + # producers by default + if self.closed: + return + self.producer_fifo.push(producer) + if send: self.initiate_send() + + push_with_producer=push + + def work(self): + "try to handle a request" + if not self.working: + if self.queue: + self.working=1 + try: module_name, request, response=self.queue.pop(0) + except: return + handle(module_name, request, response) + + def close(self): + self.closed=1 + while self.queue: + self.queue.pop() + if self.current_request is not None: + self.current_request.channel=None # break circ refs + self.current_request=None + while self.producer_fifo: + p=self.producer_fifo.first() + if p is not None and type(p) != types.StringType: + p.more() # free up resources held by producer + self.producer_fifo.pop() + self.del_channel() + #self.socket.set_shutdown(SSL.SSL_SENT_SHUTDOWN|SSL.SSL_RECEIVED_SHUTDOWN) + self.socket.close() + + def done(self): + "Called when a publishing request is finished" + self.working=0 + self.work() + + def kill_zombies(self): + now = int (time.time()) + for channel in asyncore.socket_map.values(): + if channel.__class__ == self.__class__: + if (now - channel.creation_time) > channel.zombie_timeout: + channel.close() + + +class zhttps_server(https_server): + "https server" + + SERVER_IDENT='ZServerSSL/%s' % (ZSERVER_SSL_VERSION,) + + channel_class = zhttps_channel + shutup = 0 + + def __init__(self, ip, port, ssl_ctx, resolver=None, logger_object=None): + self.shutup = 1 + https_server.__init__(self, ip, port, ssl_ctx, resolver, logger_object) + self.ssl_ctx = ssl_ctx + self.shutup = 0 + self.log_info('(%s) HTTPS server started at %s\n' + '\tHostname: %s\n\tPort: %d' % ( + self.SERVER_IDENT, + time.ctime(time.time()), + self.server_name, + self.server_port + )) + + def log_info(self, message, type='info'): + if self.shutup: return + dispatcher.log_info(self, message, type) + + def readable(self): + return self.accepting and \ + len(asyncore.socket_map) < CONNECTION_LIMIT + + def listen(self, num): + # override asyncore limits for nt's listen queue size + self.accepting = 1 + return self.socket.listen (num) + diff --git a/demo/Zope/ZServer/__init__.py b/demo/Zope/ZServer/__init__.py new file mode 100644 index 0000000..69fe9b1 --- /dev/null +++ b/demo/Zope/ZServer/__init__.py @@ -0,0 +1,95 @@ +############################################################################## +# +# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE +# +############################################################################## + +import sys, os + +# HACKERY to get around asyncore issues. This ought to go away! We're +# currently using the Python 2.2 asyncore bundled with Zope to override +# brokenness in the Python 2.1 version. We need to do some funny business +# to make this work, as a 2.2-ism crept into the asyncore code. +if os.name == 'posix': + import fcntl + if not hasattr(fcntl, 'F_GETFL'): + import FCNTL + fcntl.F_GETFL = FCNTL.F_GETFL + fcntl.F_SETFL = FCNTL.F_SETFL + +from medusa import asyncore +sys.modules['asyncore'] = asyncore + + + +from medusa.test import max_sockets +CONNECTION_LIMIT=max_sockets.max_select_sockets() + +ZSERVER_VERSION='1.1b1' +import App.FindHomes +try: + import App.version_txt + ZOPE_VERSION=App.version_txt.version_txt() +except: + ZOPE_VERSION='experimental' + + +# Try to poke zLOG default logging into asyncore +# XXX We should probably should do a better job of this, +# however that would mean that ZServer required zLOG. +try: + from zLOG import LOG, register_subsystem, BLATHER, INFO, WARNING, ERROR + register_subsystem('ZServer') + severity={'info':INFO, 'warning':WARNING, 'error': ERROR} + + def log_info(self, message, type='info'): + if message[:14]=='adding channel' or \ + message[:15]=='closing channel' or \ + message == 'Computing default hostname': + LOG('ZServer', BLATHER, message) + else: + LOG('ZServer', severity[type], message) + + import asyncore + asyncore.dispatcher.log_info=log_info +except: + pass + +# A routine to try to arrange for request sockets to be closed +# on exec. This makes it easier for folks who spawn long running +# processes from Zope code. Thanks to Dieter Maurer for this. +try: + import fcntl + try: + from fcntl import F_SETFD, FD_CLOEXEC + except ImportError: + from FCNTL import F_SETFD, FD_CLOEXEC + + def requestCloseOnExec(sock): + try: fcntl.fcntl(sock.fileno(), F_SETFD, FD_CLOEXEC) + except: pass + +except (ImportError, AttributeError): + + def requestCloseOnExec(sock): + pass + +import asyncore +from medusa import resolver, logger +from HTTPServer import zhttp_server, zhttp_handler +from HTTPS_Server import zhttps_server, zhttps0_handler, zhttps_handler +from PCGIServer import PCGIServer +from FCGIServer import FCGIServer +from FTPServer import FTPServer +from PubCore import setNumberOfThreads +from medusa.monitor import secure_monitor_server + +# override the service name in logger.syslog_logger +logger.syslog_logger.svc_name='ZServer' diff --git a/demo/Zope/ZServer/medusa/ftps_server.py b/demo/Zope/ZServer/medusa/ftps_server.py new file mode 100644 index 0000000..8fee339 --- /dev/null +++ b/demo/Zope/ZServer/medusa/ftps_server.py @@ -0,0 +1,438 @@ +"""An FTP/TLS server built on Medusa's ftp_server. + +Copyright (c) 1999-2003 Ng Pheng Siong. All rights reserved.""" + +# Python +import socket, string, sys, time + +# Medusa +from counter import counter +import asynchat, asyncore, ftp_server, logger + +# M2Crypto +from M2Crypto import SSL + +VERSION_STRING='0.09' + +class ftp_tls_channel(ftp_server.ftp_channel): + + """FTP/TLS server channel for Medusa.""" + + def __init__(self, server, ssl_ctx, conn, addr): + """Initialise the channel.""" + self.ssl_ctx = ssl_ctx + self.server = server + self.current_mode = 'a' + self.addr = addr + asynchat.async_chat.__init__(self, conn) + self.set_terminator('\r\n') + self.client_addr = (addr[0], 21) + self.client_dc = None + self.in_buffer = '' + self.closing = 0 + self.passive_acceptor = None + self.passive_connection = None + self.filesystem = None + self.authorized = 0 + self._ssl_accepting = 0 + self._ssl_accepted = 0 + self._pbsz = None + self._prot = None + resp = '220 %s M2Crypto (Medusa) FTP/TLS server v%s ready.' + self.respond(resp % (self.server.hostname, VERSION_STRING)) + + def writable(self): + return self._ssl_accepting or self._ssl_accepted + + def handle_read(self): + """Handle a read event.""" + if self._ssl_accepting: + self._ssl_accepted = self.socket.accept_ssl() + if self._ssl_accepted: + self._ssl_accepting = 0 + else: + try: + ftp_server.ftp_channel.handle_read(self) + except SSL.SSLError, what: + if str(what) == 'unexpected eof': + self.close() + else: + raise + + def handle_write(self): + """Handle a write event.""" + if self._ssl_accepting: + self._ssl_accepted = self.socket.accept_ssl() + if self._ssl_accepted: + self._ssl_accepting = 0 + else: + try: + ftp_server.ftp_channel.handle_write(self) + except SSL.SSLError, what: + if str(what) == 'unexpected eof': + self.close() + else: + raise + + def send(self, data): + """Send data over SSL.""" + try: + result = self.socket.send(data) + if result <= 0: + return 0 + else: + return result + except SSL.SSLError, what: + self.close() + self.log_info('send: closing channel %s %s' % (repr(self), what)) + return 0 + + def recv(self, buffer_size): + """Receive data over SSL.""" + try: + result = self.socket.recv(buffer_size) + if not result: + return '' + else: + return result + except SSL.SSLError, what: + self.close() + self.log_info('recv: closing channel %s %s' % (repr(self), what)) + return '' + + def found_terminator(self): + """Dispatch the FTP command.""" + line = self.in_buffer + if not len(line): + return + + sp = string.find(line, ' ') + if sp != -1: + line = [line[:sp], line[sp+1:]] + else: + line = [line] + + command = string.lower(line[0]) + if string.find(command, 'stor') != -1: + while command and command[0] not in string.letters: + command = command[1:] + + func_name = 'cmd_%s' % command + if command != 'pass': + self.log('<== %s' % repr(self.in_buffer)[1:-1]) + else: + self.log('<== %s' % line[0]+' <password>') + + self.in_buffer = '' + if not hasattr(self, func_name): + self.command_not_understood(line[0]) + return + + func = getattr(self, func_name) + if not self.check_command_authorization(command): + self.command_not_authorized(command) + else: + try: + result = apply(func, (line,)) + except: + self.server.total_exceptions.increment() + (file, func, line), t, v, tbinfo = asyncore.compact_traceback() + if self.client_dc: + try: + self.client_dc_close() + except: + pass + resp = '451 Server error: %s, %s: file %s line: %s' + self.respond(resp % (t, v, file, line)) + + def make_xmit_channel(self): + """Create a connection for sending data.""" + pa = self.passive_acceptor + if pa: + if pa.ready: + conn, addr = pa.ready + if self._prot: + cdc = tls_xmit_channel(self, conn, self.ssl_ctx, addr) + else: + cdc = ftp_server.xmit_channel(self, addr) + cdc.set_socket(conn) + cdc.connected = 1 + self.passive_acceptor.close() + self.passive_acceptor = None + else: + if self._prot: + cdc = tls_xmit_channel(self, None, self.ssl_ctx, None) + else: + cdc = ftp_server.xmit_channel(self) + else: + if self._prot: + cdc = tls_xmit_channel(self, None, self.ssl_ctx, self.client_addr) + else: + cdc = ftp_server.xmit_channel(self, self.client_addr) + cdc.create_socket(socket.AF_INET, socket.SOCK_STREAM) + if self.bind_local_minus_one: + cdc.bind(('', self.server.port - 1)) + try: + cdc.connect(self.client_addr) + except socket.error, what: + self.respond('425 Cannot build data connection') + self.client_dc = cdc + + def make_recv_channel(self, fd): + """Create a connection for receiving data.""" + pa = self.passive_acceptor + if pa: + if pa.ready: + conn, addr = pa.ready + if self._prot: + cdc = tls_recv_channel(self, conn, self.ssl_ctx, addr, fd) + else: + cdc = ftp_server.recv_channel(self, addr, fd) + cdc.set_socket(conn) + cdc.connected = 1 + self.passive_acceptor.close() + self.passive_acceptor = None + else: + if self._prot: + cdc = tls_recv_channel(self, None, self.ssl_ctx, None, fd) + else: + cdc = ftp_server.recv_channel(self, None, fd) + else: + if self._prot: + cdc = tls_recv_channel(self, None, self.ssl_ctx, self._prot, self.client_addr, fd) + else: + cdc = ftp_server.recv_channel(self, self.client_addr, fd) + cdc.create_socket(socket.AF_INET, socket.SOCK_STREAM) + try: + cdc.connect(self.client_addr) + except socket.error, what: + self.respond('425 Cannot build data connection') + self.client_dc = cdc + + def cmd_auth(self, line): + """Prepare for TLS operation.""" + # XXX Handle variations. + if line[1] != 'TLS': + self.command_not_understood (string.join(line)) + else: + self.respond('234 AUTH TLS successful') + self._ssl_accepting = 1 + self.socket = SSL.Connection(self.ssl_ctx, self.socket) + self.socket.setup_addr(self.addr) + self.socket.setup_ssl() + self.socket.set_accept_state() + self._ssl_accepted = self.socket.accept_ssl() + if self._ssl_accepted: + self._ssl_accepting = 0 + + def cmd_pbsz(self, line): + """Negotiate size of buffer for secure data transfer. For + FTP/TLS the only valid value for the parameter is '0'; any + other value is accepted but ignored.""" + if not (self._ssl_accepting or self._ssl_accepted): + return self.respond('503 AUTH TLS must be issued prior to PBSZ') + self._pbsz = 1 + self.respond('200 PBSZ=0 successful.') + + def cmd_prot(self, line): + """Negotiate the security level of the data connection.""" + if self._pbsz is None: + return self.respond('503 PBSZ must be issued prior to PROT') + if line[1] == 'C': + self.respond('200 Protection set to Clear') + self._pbsz = None + self._prot = None + elif line[1] == 'P': + self.respond('200 Protection set to Private') + self._prot = 1 + elif line[1] in ('S', 'E'): + self.respond('536 PROT %s unsupported' % line[1]) + else: + self.respond('504 PROT %s unsupported' % line[1]) + + +class ftp_tls_server(ftp_server.ftp_server): + + """FTP/TLS server for Medusa.""" + + SERVER_IDENT = 'M2Crypto FTP/TLS Server (v%s)' % VERSION_STRING + + ftp_channel_class = ftp_tls_channel + + def __init__(self, authz, ssl_ctx, host=None, ip='', port=21, resolver=None, log_obj=None): + """Initialise the server.""" + self.ssl_ctx = ssl_ctx + self.ip = ip + self.port = port + self.authorizer = authz + + if host is None: + self.hostname = socket.gethostname() + else: + self.hostname = host + + self.total_sessions = counter() + self.closed_sessions = counter() + self.total_files_out = counter() + self.total_files_in = counter() + self.total_bytes_out = counter() + self.total_bytes_in = counter() + self.total_exceptions = counter() + + asyncore.dispatcher.__init__(self) + self.create_socket(socket.AF_INET, socket.SOCK_STREAM) + self.set_reuse_addr() + self.bind((self.ip, self.port)) + self.listen(5) + + if log_obj is None: + log_obj = sys.stdout + + if resolver: + self.logger = logger.resolving_logger(resolver, log_obj) + else: + self.logger = logger.unresolving_logger(logger.file_logger(sys.stdout)) + + l = 'M2Crypto (Medusa) FTP/TLS server started at %s\n\tAuthz: %s\n\tHostname: %s\n\tPort: %d' + self.log_info(l % (time.ctime(time.time()), repr(self.authorizer), self.hostname, self.port)) + + def handle_accept(self): + """Accept a socket and dispatch a channel to handle it.""" + conn, addr = self.accept() + self.total_sessions.increment() + self.log_info('Connection from %s:%d' % addr) + self.ftp_channel_class(self, self.ssl_ctx, conn, addr) + + +class nbio_ftp_tls_actor: + + """TLS protocol negotiation mixin for FTP/TLS.""" + + def tls_init(self, sock, ssl_ctx, client_addr): + """Perform TLS protocol negotiation.""" + self.ssl_ctx = ssl_ctx + self.client_addr = client_addr + self._ssl_handshaking = 1 + self._ssl_handshake_ok = 0 + if sock: + self.socket = SSL.Connection(self.ssl_ctx, sock) + self.socket.setup_addr(self.client_addr) + self.socket.setup_ssl() + self._ssl_handshake_ok = self.socket.accept_ssl() + if self._ssl_handshake_ok: + self._ssl_handshaking = 0 + self.add_channel() + # else the client hasn't connected yet; when that happens, + # handle_connect() will be triggered. + + def tls_neg_ok(self): + """Return status of TLS protocol negotiation.""" + if self._ssl_handshaking: + self._ssl_handshake_ok = self.socket.accept_ssl() + if self._ssl_handshake_ok: + self._ssl_handshaking = 0 + return self._ssl_handshake_ok + + def handle_connect(self): + """Handle a data connection that occurs after this instance came + into being. When this handler is triggered, self.socket has been + created and refers to the underlying connected socket.""" + self.socket = SSL.Connection(self.ssl_ctx, self.socket) + self.socket.setup_addr(self.client_addr) + self.socket.setup_ssl() + self._ssl_handshake_ok = self.socket.accept_ssl() + if self._ssl_handshake_ok: + self._ssl_handshaking = 0 + self.add_channel() + + def send(self, data): + """Send data over SSL.""" + try: + result = self.socket.send(data) + if result <= 0: + return 0 + else: + return result + except SSL.SSLError, what: + self.close() + self.log_info('send: closing channel %s %s' % (repr(self), what)) + return 0 + + def recv(self, buffer_size): + """Receive data over SSL.""" + try: + result = self.socket.recv(buffer_size) + if not result: + return '' + else: + return result + except SSL.SSLError, what: + self.close() + self.log_info('recv: closing channel %s %s' % (repr(self), what)) + return '' + + +class tls_xmit_channel(nbio_ftp_tls_actor, ftp_server.xmit_channel): + + """TLS driver for a send-only data connection.""" + + def __init__(self, channel, conn, ssl_ctx, client_addr=None): + """Initialise the driver.""" + ftp_server.xmit_channel.__init__(self, channel, client_addr) + self.tls_init(conn, ssl_ctx, client_addr) + + def readable(self): + """This channel is readable iff TLS negotiation is in progress. + (Which implies a connected channel, of course.)""" + if not self.connected: + return 0 + else: + return self._ssl_handshaking + + def writable(self): + """This channel is writable iff TLS negotiation is in progress + or the application has data to send.""" + if self._ssl_handshaking: + return 1 + else: + return ftp_server.xmit_channel.writable(self) + + def handle_read(self): + """Handle a read event: either continue with TLS negotiation + or let the application handle this event.""" + if self.tls_neg_ok(): + ftp_server.xmit_channel.handle_read(self) + + def handle_write(self): + """Handle a write event: either continue with TLS negotiation + or let the application handle this event.""" + if self.tls_neg_ok(): + ftp_server.xmit_channel.handle_write(self) + + +class tls_recv_channel(nbio_ftp_tls_actor, ftp_server.recv_channel): + + """TLS driver for a receive-only data connection.""" + + def __init__(self, channel, conn, ssl_ctx, client_addr, fd): + """Initialise the driver.""" + ftp_server.recv_channel.__init__(self, channel, client_addr, fd) + self.tls_init(conn, ssl_ctx, client_addr) + + def writable(self): + """This channel is writable iff TLS negotiation is in progress.""" + return self._ssl_handshaking + + def handle_read(self): + """Handle a read event: either continue with TLS negotiation + or let the application handle this event.""" + if self.tls_neg_ok(): + ftp_server.recv_channel.handle_read(self) + + def handle_write(self): + """Handle a write event: either continue with TLS negotiation + or let the application handle this event.""" + if self.tls_neg_ok(): + ftp_server.recv_channel.handle_write(self) + + diff --git a/demo/Zope/ZServer/medusa/https_server.py b/demo/Zope/ZServer/medusa/https_server.py new file mode 100644 index 0000000..9b932b7 --- /dev/null +++ b/demo/Zope/ZServer/medusa/https_server.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python + +"""A https server built on Medusa's http_server. + +Copyright (c) 1999-2004 Ng Pheng Siong. All rights reserved.""" + +import asynchat, asyncore, http_server, socket, sys +from M2Crypto import SSL, version + +VERSION_STRING=version + +class https_channel(http_server.http_channel): + + ac_in_buffer_size = 1 << 16 + + def __init__(self, server, conn, addr): + http_server.http_channel.__init__(self, server, conn, addr) + + def send(self, data): + try: + result = self.socket._write_nbio(data) + if result <= 0: + return 0 + else: + self.server.bytes_out.increment(result) + return result + except SSL.SSLError, why: + self.close() + self.log_info('send: closing channel %s %s' % (repr(self), why)) + return 0 + + def recv(self, buffer_size): + try: + result = self.socket._read_nbio(buffer_size) + if result is None: + return '' + elif result == '': + self.close() + return '' + else: + self.server.bytes_in.increment(len(result)) + return result + except SSL.SSLError, why: + self.close() + self.log_info('recv: closing channel %s %s' % (repr(self), why)) + return '' + + +class https_server(http_server.http_server): + + SERVER_IDENT='M2Crypto HTTPS Server (v%s)' % VERSION_STRING + + channel_class=https_channel + + def __init__(self, ip, port, ssl_ctx, resolver=None, logger_object=None): + http_server.http_server.__init__(self, ip, port, resolver, logger_object) + self.ssl_ctx=ssl_ctx + + def handle_accept(self): + # Cribbed from http_server. + self.total_clients.increment() + try: + conn, addr = self.accept() + except socket.error: + # linux: on rare occasions we get a bogus socket back from + # accept. socketmodule.c:makesockaddr complains that the + # address family is unknown. We don't want the whole server + # to shut down because of this. + sys.stderr.write ('warning: server accept() threw an exception\n') + return + + # Turn the vanilla socket into an SSL connection. + try: + ssl_conn=SSL.Connection(self.ssl_ctx, conn) + ssl_conn._setup_ssl(addr) + ssl_conn.accept_ssl() + self.channel_class(self, ssl_conn, addr) + except SSL.SSLError: + pass + + def writeable(self): + return 0 + diff --git a/demo/Zope/ca.pem b/demo/Zope/ca.pem new file mode 100644 index 0000000..b7c84a1 --- /dev/null +++ b/demo/Zope/ca.pem @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDWTCCAsKgAwIBAgIBADANBgkqhkiG9w0BAQQFADCBgDELMAkGA1UEBhMCU0cx +ETAPBgNVBAoTCE0yQ3J5cHRvMRQwEgYDVQQLEwtNMkNyeXB0byBDQTEkMCIGA1UE +AxMbTTJDcnlwdG8gQ2VydGlmaWNhdGUgTWFzdGVyMSIwIAYJKoZIhvcNAQkBFhNu +Z3BzQG5ldG1lbWV0aWMuY29tMB4XDTAxMTIxNTA1NTU0NloXDTA0MTIxNDA1NTU0 +NlowgYAxCzAJBgNVBAYTAlNHMREwDwYDVQQKEwhNMkNyeXB0bzEUMBIGA1UECxML +TTJDcnlwdG8gQ0ExJDAiBgNVBAMTG00yQ3J5cHRvIENlcnRpZmljYXRlIE1hc3Rl +cjEiMCAGCSqGSIb3DQEJARYTbmdwc0BuZXRtZW1ldGljLmNvbTCBnzANBgkqhkiG +9w0BAQEFAAOBjQAwgYkCgYEAx8soJbS719LHK62VVVIQeC3oW0HvFArwPnA0LuEK +q+LaqMOJg1rS7hvFdX03diV+XJw7cC0iECZYJNG4ii1xbY6KRmufkInaAwm54E3N +e+YYVocaqUkcN6xVf6fwnLfPXbpFS/K2Umg11ObKMmi80JmiIdjcjRRCQZC7g1hf +q+kCAwEAAaOB4DCB3TAdBgNVHQ4EFgQU6/qcBzEtQphfXLhiOHbt2KqBwMIwga0G +A1UdIwSBpTCBooAU6/qcBzEtQphfXLhiOHbt2KqBwMKhgYakgYMwgYAxCzAJBgNV +BAYTAlNHMREwDwYDVQQKEwhNMkNyeXB0bzEUMBIGA1UECxMLTTJDcnlwdG8gQ0Ex +JDAiBgNVBAMTG00yQ3J5cHRvIENlcnRpZmljYXRlIE1hc3RlcjEiMCAGCSqGSIb3 +DQEJARYTbmdwc0BuZXRtZW1ldGljLmNvbYIBADAMBgNVHRMEBTADAQH/MA0GCSqG +SIb3DQEBBAUAA4GBAD+I14GuS5vJmyv1k7mUMbAicsWRHZ+zrGOq9L/L2LsA+lKQ +dAzEZE2+Zv8LBPJVltbJJhcFNJS/ZMAjEm4xlJuCpvXVMxd/M5AM29aqekWlIK7J +vsdDL8IuzpRkMniUiNKPhmB6IPIOslvUKx6QofcE0wDh6pg4VvIbCjkpZ7gf +-----END CERTIFICATE----- diff --git a/demo/Zope/dh1024.pem b/demo/Zope/dh1024.pem new file mode 100644 index 0000000..81d43f6 --- /dev/null +++ b/demo/Zope/dh1024.pem @@ -0,0 +1,5 @@ +-----BEGIN DH PARAMETERS----- +MIGHAoGBAJf2QmHKtQXdKCjhPx1ottPb0PMTBH9A6FbaWMsTuKG/K3g6TG1Z1fkq +/Gz/PWk/eLI9TzFgqVAuPvr3q14a1aZeVUMTgo2oO5/y2UHe6VaJ+trqCTat3xlx +/mNbIK9HA2RgPC3gWfVLZQrY+gz3ASHHR5nXWHEyvpuZm7m3h+irAgEC +-----END DH PARAMETERS----- diff --git a/demo/Zope/lib/python/Products/GuardedFile/GuardedFile.py b/demo/Zope/lib/python/Products/GuardedFile/GuardedFile.py new file mode 100644 index 0000000..1e3137f --- /dev/null +++ b/demo/Zope/lib/python/Products/GuardedFile/GuardedFile.py @@ -0,0 +1,53 @@ +"""GuardedFile.GuardedFile + +Copyright (c) 2000-2003 Ng Pheng Siong. All rights reserved. +This software is released under the ZPL. Usual disclaimers apply.""" + +__version__ = '1.3' + +from AccessControl import getSecurityManager +from Globals import HTMLFile, MessageDialog +from OFS.Image import File, cookId + +manage_addForm = HTMLFile('add', globals(),Kind='GuardedFile',kind='GuardedFile') +def manage_addGuardedFile(self, id, file, title='', precondition='', content_type='', REQUEST=None): + """ + Add a new GuardedFile object. + + Creates a new GuardedFile object 'id' with the content of 'file'. + """ + # Object creation stuff, cribbed from OFS.Image.manage_addFile(). + id, title = cookId(id, title, file) + self = self.this() + self._setObject(id, GuardedFile(id, title, '', content_type, precondition)) + obj = self._getOb(id) + obj.manage_upload(file) + + # Unset permission acquisition. + obj.manage_acquiredPermissions() + + # Create a proxy role and set a specific permission for it. + proxy_role = "proxy_for_%s" % id + self._addRole(proxy_role) + obj.manage_role(proxy_role, ['View']) + uname = getSecurityManager().getUser().getUserName() + self.manage_addLocalRoles(uname, (proxy_role,), REQUEST) + + # Feedback. + if REQUEST: return MessageDialog( + title ='Success!', + message='GuardedFile "%s" has been created.' % id, + action ='manage_main') + + +class GuardedFile(File): + """A File object accessible by proxy only.""" + meta_type = "GuardedFile" + + def manage_beforeDelete(self, item, container): + """Delete self's proxy role.""" + role = "proxy_for_%s" % self.__name__ + container._delRoles([role], None) + self.manage_delLocalRoles(self.users_with_local_role(role)) + + diff --git a/demo/Zope/lib/python/Products/GuardedFile/README.txt b/demo/Zope/lib/python/Products/GuardedFile/README.txt new file mode 100644 index 0000000..d35b9a5 --- /dev/null +++ b/demo/Zope/lib/python/Products/GuardedFile/README.txt @@ -0,0 +1,16 @@ +GuardedFile + + A GuardedFile is a Zope File that is accessible *by proxy* only. + + When a GuardedFile is created, all acquired permissions are unset. + A proxy role is created in its container with the sole permission + "View". + + When the GuardedFile is deleted, its associated proxy role is also + removed. + + In all other aspects a GuardedFile behaves exactly like a File. + + + $Id: README.txt 299 2005-06-09 17:32:28Z heikki $ + $Revision: 1.2 $ diff --git a/demo/Zope/lib/python/Products/GuardedFile/TODO.txt b/demo/Zope/lib/python/Products/GuardedFile/TODO.txt new file mode 100644 index 0000000..8afc8c7 --- /dev/null +++ b/demo/Zope/lib/python/Products/GuardedFile/TODO.txt @@ -0,0 +1 @@ +1. Icon. diff --git a/demo/Zope/lib/python/Products/GuardedFile/__init__.py b/demo/Zope/lib/python/Products/GuardedFile/__init__.py new file mode 100644 index 0000000..f587c75 --- /dev/null +++ b/demo/Zope/lib/python/Products/GuardedFile/__init__.py @@ -0,0 +1,25 @@ +"""GuardedFile.__init__ + +Copyright (c) 2000 Ng Pheng Siong. All rights reserved. +This software is released under the ZPL.""" + +__version__ = '1.1' + +import GuardedFile + +def initialize(context): + try: + context.registerClass( + GuardedFile.GuardedFile, + constructors=(GuardedFile.manage_addForm, GuardedFile.manage_addGuardedFile) + #icon='folder.gif' + ) + context.registerBaseClass(GuardedFile.GuardedFile) + + except: + import sys, traceback, string + type, val, tb = sys.exc_info() + sys.stderr.write(string.join( + traceback.format_exception(type, val, tb),'')) + del type, val, tb + diff --git a/demo/Zope/lib/python/Products/GuardedFile/add.dtml b/demo/Zope/lib/python/Products/GuardedFile/add.dtml new file mode 100644 index 0000000..ed78fa6 --- /dev/null +++ b/demo/Zope/lib/python/Products/GuardedFile/add.dtml @@ -0,0 +1,78 @@ +<dtml-var manage_page_header> + +<dtml-var "manage_form_title(this(), _, form_title='Add GuardedFile')"> + +<p class="form-help"> + A GuardedFile is a Zope File that is accessible <em>by proxy</em> only. +</p> + +<p class="form-help"> + When a GuardedFile is created, all acquired permissions are unset. + A proxy role is created in its container with the sole permission + "View". +</p> + +<p class="form-help"> + When the GuardedFile is deleted, its associated proxy role is also + removed. +</p> + +<p class="form-help"> + In all other aspects a GuardedFile behaves exactly like a File. +</p> + +<p class="form-help"> + You can create a new <dtml-var kind> using the form below. + Select a file from your local computer by clicking the + <em>Browse</em> button. The file you select will be uploaded + to Zope as a <dtml-var kind>. +</P> + +<FORM ACTION="manage_add<dtml-var Kind>" METHOD="POST" + ENCTYPE="multipart/form-data"> +<table cellspacing="0" cellpadding="2" border="0"> + <tr> + <td align="left" valign="top"> + <div class="form-label"> + Id + </div> + </td> + <td align="left" valign="top"> + <input type="text" name="id" size="40" /> + </td> + </tr> + <tr> + <td align="left" valign="top"> + <div class="form-optional"> + Title + </div> + </td> + <td align="left" valign="top"> + <input type="text" name="title" size="40" /> + </td> + </tr> + <tr> + <td align="left" valign="top"> + <div class="form-label"> + File + </div> + </td> + <td align="left" valign="top"> + <input type="file" name="file:string" size="25" value="" /> + </td> + </tr> + <tr> + <td align="left" valign="top"> + </td> + <td align="left" valign="top"> + <div class="form-element"> + <input class="form-element" type="submit" name="submit" + value=" Add " /> + </div> + </td> + </tr> +</TABLE> +</FORM> + +<dtml-var manage_page_footer> + diff --git a/demo/Zope/lib/python/Products/GuardedFile/refresh.txt b/demo/Zope/lib/python/Products/GuardedFile/refresh.txt new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/demo/Zope/lib/python/Products/GuardedFile/refresh.txt diff --git a/demo/Zope/lib/python/Products/GuardedFile/version.txt b/demo/Zope/lib/python/Products/GuardedFile/version.txt new file mode 100644 index 0000000..1e5ebbf --- /dev/null +++ b/demo/Zope/lib/python/Products/GuardedFile/version.txt @@ -0,0 +1 @@ +GuardedFile-1-1-0 diff --git a/demo/Zope/lib/python/Products/ZSmime/README.txt b/demo/Zope/lib/python/Products/ZSmime/README.txt new file mode 100644 index 0000000..f43740b --- /dev/null +++ b/demo/Zope/lib/python/Products/ZSmime/README.txt @@ -0,0 +1,18 @@ +ZSmime enables Zope to generate S/MIME-signed/encrypted messages. + +ZSmime is useful where Zope accepts confidential information over the +web, e.g., credit card information, Swiss bank account instructions, etc. +Such information can be protected by ZSmime and relayed off-site +immediately. This reduces the value of the information carried on-site +and in turn reduces the effect of a successful attack against the site. + +Even if the S/MIME-protected information remains on-site, it is now +encrypted - this introduces additional cost in defeating the protection +and may mitigate the impact of a successful site penetration. + +ZSmime adds a DTML tag "dtml-smime" to Zope. + + +$Id: README.txt 299 2005-06-09 17:32:28Z heikki $ +$Revision: 1.1 $ + diff --git a/demo/Zope/lib/python/Products/ZSmime/SmimeTag.py b/demo/Zope/lib/python/Products/ZSmime/SmimeTag.py new file mode 100644 index 0000000..142bec9 --- /dev/null +++ b/demo/Zope/lib/python/Products/ZSmime/SmimeTag.py @@ -0,0 +1,104 @@ +"""ZSmime.SmimeTag + +Copyright (c) 1999-2001 Ng Pheng Siong. All rights reserved. +This software is released under the ZPL. Usual disclaimers apply.""" + +__version__ = '1.2' + +# Zope tag stuff. +from DocumentTemplate.DT_String import String +from DocumentTemplate.DT_Util import * +from DocumentTemplate.DT_Var import Var, Call + +# M2Crypto. +from M2Crypto import BIO, SMIME, X509 + +SmimeError = "SmimeTag Error" + +class SmimeTag: + """<dtml-smime>""" + + name = 'smime' + blockContinuations = () + + def __init__(self, blocks): + tname, args, section = blocks[0] + self.section = section + + args = parse_params(args, signer=None, recipients=None) + has_key = args.has_key + + if has_key('signer'): + self.signer = args['signer'] + try: + Call(self.signer) + except ParseError: + raise SmimeError, ('Invalid parameter "signer".') + else: + raise SmimeError, ('The parameter "signer" was not specified in tag.') + + if has_key('recipients'): + self.recipients = args['recipients'] + try: + Call(self.recipients) + except ParseError: + raise SmimeError, ('Invalid parameter "recipients".') + else: + raise SmimeError, ('The parameter "recipients" was not specified in tag.') + + + def render(self, md): + # Render the dtml block. + data = render_blocks(self.section.blocks, md) + data_bio = BIO.MemoryBuffer(data) + + # Prepare to S/MIME. + s = SMIME.SMIME() + + # Render the signer key, load into BIO. + try: + signer = Var(self.signer).render(md) + except ParseError: + raise SmimeError, ('Invalid parameter "signer".') + signer_key_bio = BIO.MemoryBuffer(signer) + signer_cert_bio = BIO.MemoryBuffer(signer) # XXX Kludge. + + # Sign the data. + s.load_key_bio(signer_key_bio, signer_cert_bio) + p7 = s.sign(data_bio, flags=SMIME.PKCS7_TEXT) + + # Recreate coz sign() has consumed the MemoryBuffer. + # May be cheaper to seek to start. + data_bio = BIO.MemoryBuffer(data) + + # Render recipients, load into BIO. + try: + recip = Var(self.recipients).render(md) + except ParseError: + raise SmimeError, ('Invalid parameter "recipients".') + recip_bio = BIO.MemoryBuffer(recip) + + # Load recipient certificates. + sk = X509.X509_Stack() + sk.push(X509.load_cert_bio(recip_bio)) + s.set_x509_stack(sk) + + # Set a cipher. + s.set_cipher(SMIME.Cipher('des_ede3_cbc')) + + # Encrypt. + tmp_bio = BIO.MemoryBuffer() + s.write(tmp_bio, p7) + p7 = s.encrypt(tmp_bio) + + # Finally, return the now signed/encrypted PKCS7. + out = BIO.MemoryBuffer() + s.write(out, p7) + return out.getvalue() + + + __call__ = render + + +String.commands['smime'] = SmimeTag + diff --git a/demo/Zope/lib/python/Products/ZSmime/__init__.py b/demo/Zope/lib/python/Products/ZSmime/__init__.py new file mode 100644 index 0000000..ebb77f6 --- /dev/null +++ b/demo/Zope/lib/python/Products/ZSmime/__init__.py @@ -0,0 +1,9 @@ +"""ZSmime.__init__ + +Copyright (c) 2000 Ng Pheng Siong. All rights reserved. +This software is released under the ZPL. Usual disclaimers apply.""" + +__version__='1.1' + +import SmimeTag + diff --git a/demo/Zope/lib/python/Products/ZSmime/version.txt b/demo/Zope/lib/python/Products/ZSmime/version.txt new file mode 100644 index 0000000..ac26a94 --- /dev/null +++ b/demo/Zope/lib/python/Products/ZSmime/version.txt @@ -0,0 +1 @@ +ZSmime-1-0-1 diff --git a/demo/Zope/server.pem b/demo/Zope/server.pem new file mode 100644 index 0000000..1ee9282 --- /dev/null +++ b/demo/Zope/server.pem @@ -0,0 +1,36 @@ +-----BEGIN CERTIFICATE----- +MIIDYjCCAsugAwIBAgIBBDANBgkqhkiG9w0BAQQFADCBgDELMAkGA1UEBhMCU0cx +ETAPBgNVBAoTCE0yQ3J5cHRvMRQwEgYDVQQLEwtNMkNyeXB0byBDQTEkMCIGA1UE +AxMbTTJDcnlwdG8gQ2VydGlmaWNhdGUgTWFzdGVyMSIwIAYJKoZIhvcNAQkBFhNu +Z3BzQG5ldG1lbWV0aWMuY29tMB4XDTAzMDYyMjEzMzAxNFoXDTA0MDYyMTEzMzAx +NFowXTELMAkGA1UEBhMCU0cxETAPBgNVBAoTCE0yQ3J5cHRvMRIwEAYDVQQDEwls +b2NhbGhvc3QxJzAlBgkqhkiG9w0BCQEWGGFkbWluQHNlcnZlci5leGFtcGxlLmRv +bTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA37aKGZtFicl8xXXTLJ8/JD7c +kd3t/teCX9i61lpaDQCKBoVrrursIvZihemMKI9g/u/+BLqt5g8mBdgUdYz0txc8 +KEbV2hj+wwOX4H3XwD0Y+DysXiNHq7/tFdmzSVHoLxpY4zYzXbxQ/p049wvIyPRp +/y3omcnx/TEUhkn+JmkCAwEAAaOCAQwwggEIMAkGA1UdEwQCMAAwLAYJYIZIAYb4 +QgENBB8WHU9wZW5TU0wgR2VuZXJhdGVkIENlcnRpZmljYXRlMB0GA1UdDgQWBBTB +H/mUYlww24mJlSxEtGdlwojO9zCBrQYDVR0jBIGlMIGigBTr+pwHMS1CmF9cuGI4 +du3YqoHAwqGBhqSBgzCBgDELMAkGA1UEBhMCU0cxETAPBgNVBAoTCE0yQ3J5cHRv +MRQwEgYDVQQLEwtNMkNyeXB0byBDQTEkMCIGA1UEAxMbTTJDcnlwdG8gQ2VydGlm +aWNhdGUgTWFzdGVyMSIwIAYJKoZIhvcNAQkBFhNuZ3BzQG5ldG1lbWV0aWMuY29t +ggEAMA0GCSqGSIb3DQEBBAUAA4GBAAvl6v0s3eFeGP4iAcrfysuK7jzFKhjDYuOy +lVS3u33bZNLnMpM6OSEM9yPh4WpFCVHf+nYwC71pk4ilsLVXjKxymm2lNGcxLVuW +iydFz4Ly9nmN7Ja9ygYT39dGAFP/wN7ELTpsbul8VfmqhNg9y81d8i/A1tK3AGA8 +0QkPQNdP +-----END CERTIFICATE----- +-----BEGIN RSA PRIVATE KEY----- +MIICXAIBAAKBgQDftooZm0WJyXzFddMsnz8kPtyR3e3+14Jf2LrWWloNAIoGhWuu +6uwi9mKF6Ywoj2D+7/4Euq3mDyYF2BR1jPS3FzwoRtXaGP7DA5fgfdfAPRj4PKxe +I0erv+0V2bNJUegvGljjNjNdvFD+nTj3C8jI9Gn/LeiZyfH9MRSGSf4maQIDAQAB +AoGAHpeVtv62uarl9xKvuBBm0AwQmZnhq9HIsFaw5hMg8Vo7hbzFBvx1IirTOkC/ +u+QvfW1QLVFh6m3z4ySzV4fZBtcd6F9SbSrZ0xsxIUB2NOSa1RGgiaP61bJnMMM1 +xM3O9iwM5GZc3Gqy08QOCpDl0772VJ+9Gz3FA88mrc6rHQkCQQDz6RIatFjT28n8 +1vy0nHwwZz2oXTpe/pyZPwoKj8zVsmrKhKwOw7l8ArxjP8zoHOE7AQBCXYDMNoFp +IAF0yuqrAkEA6s0wMEdPpQeb0XHAfccyJQoeULxHdVFoz1wWmGSOm4YmQtR8/QJx +luEgfpeRkzxBKt5Ls3MEkheIOw7xV24zOwJAMz+DaE0AZPNHS3K4ghJnHZxzng6I +lzEUIjbWm0V/ml70hTy/EhMZw+6nOotLOHHo+QbK0SboSwAgzL/Gzo1cJQJANqpS +38qadleRJXAQWrg3qnvyluVe1aeAeVZ9RDmVIgxXeBO0jcs12uTLBe4PzHGo0mwy +v7K1i7XC180gzzQu5QJBAOxITT9RoWSSozPvnirHd37sn+RsrNYkV07NAa80M20Z +DkBPHeMVkNgigrQ6L6vWmbRDGQbGcMplAxnI5ppKCoU= +-----END RSA PRIVATE KEY----- diff --git a/demo/Zope/starts b/demo/Zope/starts new file mode 100644 index 0000000..2676243 --- /dev/null +++ b/demo/Zope/starts @@ -0,0 +1,17 @@ +#!/bin/sh +umask 077 +reldir=`dirname $0` +cwd=`cd $reldir; pwd` +# Zope's event logger is controlled by the "EVENT_LOG_FILE" environment +# variable. If you don't have a "EVENT_LOG_FILE" environment variable +# (or its older alias "STUPID_LOG_FILE") set, Zope will log to the standard +# output. For more information on EVENT_LOG_FILE, see doc/ENVIRONMENT.txt. +ZLOGFILE=$EVENT_LOG_FILE +if [ -z "$ZLOGFILE" ]; then + ZLOGFILE=$STUPID_LOG_FILE +fi +if [ -z "$ZLOGFILE" ]; then + EVENT_LOG_FILE="" + export EVENT_LOG_FILE +fi +exec /usr/local/bin/python2.1 $cwd/z2s.py -D "$@" diff --git a/demo/Zope/starts.bat b/demo/Zope/starts.bat new file mode 100755 index 0000000..c8bd3b6 --- /dev/null +++ b/demo/Zope/starts.bat @@ -0,0 +1 @@ +"C:\pkg\zope260\bin\python.exe" "C:\pkg\zope260\z2s.py" -D %1 %2 %3 %4 %5 %6 %7 %8 %9 diff --git a/demo/Zope/utilities/x509_user.py b/demo/Zope/utilities/x509_user.py new file mode 100644 index 0000000..1d350d6 --- /dev/null +++ b/demo/Zope/utilities/x509_user.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python + +""" +This is a very simple program to manage the access_x509 database. The +overriding goal is program portability, hence its use of 'anydbm'. + +Invoke it thusly: + + x509_user.py + -u <username> + [ -x <X.509 subject DN> ] + [ -f <database> ] + +<username> is the Zope username; it must be present. + +<X.509 subject DN> is the X.509 certificate's subject distinguished name +to associate with the user. If it is present, the association is created +or updated. If it is absent, the association is removed. + +<database> defaults to 'access_x509'. + +(I told you this is a dumb program.) + + +To read the subject distinguished name from the certificate 'client.pem', +invoke 'openssl' thusly: + + openssl x509 -subject -noout -in client.pem + +This produces the output: + + subject=/C=SG/O=M2Crypto Client/CN=M2Crypto Client/Email=ngps@post1.com + + +Next, invoke this tool: + + x509_user.py -u superuser \\ + -f "/C=SG/O=M2Crypto Client/CN=M2Crypto Client/Email=ngps@post1.com" + +This associates the user who owns client.pem to the Zope "superuser". + + +Copyright (c) 2000 Ng Pheng Siong. This program is released under the ZPL. +""" + +import anydbm, getopt, sys + +x509_db = 'access_x509' +username = subject_dn = None + +argerr='Usage' + +optlist, optarg=getopt.getopt(sys.argv[1:], 'f:u:x:') # ;-) +for opt in optlist: + if '-f' in opt: + x509_db = opt[1] + elif '-u' in opt: + username = opt[1] + elif '-x' in opt: + subject_dn = opt[1] + +if username is None: + raise argerr, '\n' + __doc__ + +db = anydbm.open(x509_db, 'cw') +if subject_dn is None: + # Remove the association... + try: + subject_dn = db[username] + del db[subject_dn] + del db[username] + except: + pass +else: + # Create/update the association. + db[subject_dn] = username + db[username] = subject_dn +db.close() + diff --git a/demo/Zope/z2s.py b/demo/Zope/z2s.py new file mode 100644 index 0000000..33885ab --- /dev/null +++ b/demo/Zope/z2s.py @@ -0,0 +1,1078 @@ +############################################################################## +# +# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE +# +############################################################################## +"""Zope 2 ZServer start-up file + +Usage: %(program)s [options] [environment settings] + +Options: + + -h + + Output this text. + + -z path + + The location of the Zope installation. + The default is the location of this script, %(here)s. + + -Z 0 or 1 + + UNIX only! This option is ignored on Windows. + + This option controls whether a management process will be created + that restarts Zope after a shutdown or crash. + + If the argument to -Z is non-null (e.g. "-Z1" or "-Zyes"), a + management process will be used. If the argument to -Z is "-", or + "0", (e.g. "-Z-" or "-Z0"), a management process will not be used. + On UNIX, the default behavior is to create a separate management + process (e.g. -Z1) if the -Z option is not specified. + + (Note: the -Z option in Zopes before Zope 2.6 used to be used to specify + a pidfile name for the management process. This pidfile no longer + exists). + + -t n + + The number of threads to use, if ZODB3 is used. The default is + %(NUMBER_OF_THREADS)s. + + -i n + + Set the interpreter check interval. This integer value + determines how often the interpreter checks for periodic things + such as thread switches and signal handlers. The Zope default + is 500, but you may want to experiment with other values that + may increase performance in your particular environment. + + -D + + Run in Zope debug mode. This causes the Zope process not to + detach from the controlling terminal, and is equivalent to + supplying the environment variable setting Z_DEBUG_MODE=1 + + -a ipaddress + + The IP address to listen on. If this is an empty string + (e.g. -a ''), then all addresses on the machine are used. The + default is %(IP_ADDRESS)s. + + -d ipaddress + + IP address of your DNS server. If this is an empty string + (e.g. -d ''), then IP addresses will not be logged. If you have + DNS service on your local machine then you can set this to + 127.0.0.1. The default is: %(DNS_IP)s. + + -u username or uid number + + The username to run ZServer as. You may want to run ZServer as + a dedicated user. This only works under Unix, and if ZServer + is started as root, and is required in that case. + + -P [ipaddress:]number + + Set the web, ftp and monitor port numbers simultaneously + as offsets from the number. The web port number will be number+80. + The FTP port number will be number+21. The monitor port number will + be number+99. + + The number can be preeceeded by an ip address follwed by a colon + to specify an address to listen on. This allows different servers + to listen on different addresses. + + Multiple -P options can be provided to run multiple sets of servers. + + -w port + + The Web server (HTTP) port. This defaults to %(HTTP_PORT)s. The + standard port for HTTP services is 80. If this is a dash + (e.g. -w -), then HTTP is disabled. + + The number can be preeceeded by an ip address follwed by a colon + to specify an address to listen on. This allows different servers + to listen on different addresses. + + Multiple -w options can be provided to run multiple servers. + + -y port + + The SSL Web server (HTTPS) port. This defaults to %(HTTPS_PORT)s. The + standard port for HTTPS services is 443. If this is a dash + (e.g. -y -), then HTTPS is disabled. + + The number can be preeceeded by an ip address follwed by a colon + to specify an address to listen on. This allows different servers + to listen on different addresses. + + Multiple -y options can be provided to run multiple servers. + + -W port + + The "WebDAV source" port. If this is a dash (e.g. -W -), then + "WebDAV source" is disabled. The default is disabled. Note that + this feature is a workaround for the lack of "source-link" support + in standard WebDAV clients. + + The port can be preeceeded by an ip address follwed by a colon + to specify an address to listen on. This allows different servers + to listen on different addresses. + + Multiple -W options can be provided to run multiple servers. + + -Y port + + The "WebDAV source over HTTPS" port. If this is a dash (e.g. -Y -), then + "WebDAV source over HTTPS" is disabled. The default is disabled. Note that + this feature is a workaround for the lack of "source-link" support + in standard WebDAV clients. + + The port can be preeceeded by an ip address follwed by a colon + to specify an address to listen on. This allows different servers + to listen on different addresses. + + Multiple -Y options can be provided to run multiple servers. + + -x + + If present, this option causes Zope to run in X.509 certificate-based + authentication mode. + + -C + --force-http-connection-close + + If present, this option causes Zope to close all HTTP connections, + regardless of the 'Connection:' header (or lack of one) sent by + the client. + + -f port + + The FTP port. If this is a dash (e.g. -f -), then FTP + is disabled. The standard port for FTP services is 21. The + default is %(FTP_PORT)s. + + The port can be preeceeded by an ip address follwed by a colon + to specify an address to listen on. This allows different servers + to listen on different addresses. + + Multiple -f options can be provided to run multiple servers. + + -p path + + Path to the PCGI resource file. The default value is + %(PCGI_FILE)s, relative to the Zope location. If this is a dash + (-p -) or the file does not exist, then PCGI is disabled. + + -F path_or_port + + Either a port number (for inet sockets) or a path name (for unix + domain sockets) for the FastCGI Server. If the flag and value are + not specified then the FastCGI Server is disabled. + + -m port + + The secure monitor server port. If this is a dash + (-m -), then the monitor server is disabled. The monitor server + allows interactive Python style access to a running ZServer. To + access the server see medusa/monitor_client.py or + medusa/monitor_client_win32.py. The monitor server password is the + same as the Zope emergency user password set in the 'access' + file. The default is to not start up a monitor server. + + The port can be preeceeded by an ip address follwed by a colon + to specify an address to listen on. This allows different servers + to listen on different addresses. + + Multiple -m options can be provided to run multiple servers. + + --icp port + + The ICP port. ICP can be used to distribute load between back-end + zope servers, if you are using an ICP-aware front-end proxy such + as Squid. + + The port can be preeceeded by an ip address follwed by a colon + to specify an address to listen on. This allows different servers + to listen on different addresses. + + Multiple --icp options can be provided to run multiple servers. + + -l path + + Path to the ZServer log file. If this is a relative path then the + log file will be written to the 'var' directory. The default is + %(LOG_FILE)s. + + -r + + Run ZServer is read-only mode. ZServer won't write anything to disk. + No log files, no pid files, nothing. This means that you can't do a + lot of stuff like use PCGI, and zdaemon. ZServer will log hits to + STDOUT and zLOG will log to STDERR. + + -L + + Enable locale (internationalization) support. The value passed for + this option should be the name of the locale to be used (see your + operating system documentation for locale information specific to + your system). If an empty string is passed for this option (-L ''), + Zope will set the locale to the user's default setting (typically + specified in the $LANG environment variable). If your Python + installation does not support the locale module, the requested + locale is not supported by your system or an empty string was + passed but no default locale can be found, an error will be raised + and Zope will not start. + + -X + + Disable servers. This might be used to effectively disable all + default server settings or previous server settings in the option + list before providing new settings. For example to provide just a + web server: + + %(program)s -X -w80 + + -M file + + Save detailed logging information to the given file. + This log includes separate entries for: + + - The start of a request, + - The start of processing the request in an application thread, + - The start of response output, and + - The end of the request. + +Environment settings are of the form: NAME=VALUE. + +Note: you *must* use Python 2.1 or later! +""" + + +# This is required path hackery for the win32 binary distribution +# that ensures that the bundled python libraries are used. In a +# win32 binary distribution, the installer will have replaced the +# marker string with the actual software home. If that has not +# happened, then the path munging code is skipped. +swhome=r'INSERT_SOFTWARE_HOME' +if swhome != 'INSERT_SOFTWARE_HOME': + import sys + sys.path.insert(0, '%s/lib/python' % swhome) + sys.path.insert(1, '%s/bin/lib' % swhome) + sys.path.insert(2, '%s/bin/lib/plat-win' % swhome) + sys.path.insert(3, '%s/bin/lib/win32' % swhome) + sys.path.insert(4, '%s/bin/lib/win32/lib' % swhome) + sys.path.insert(5, '%s' % swhome) + + +import os, sys, getopt, codecs, string +import socket + +from types import StringType, IntType +# workaround to allow unicode encoding conversions in DTML +dummy = codecs.lookup('iso-8859-1') + +sys.setcheckinterval(500) + +program=sys.argv[0] +here=os.path.join(os.getcwd(), os.path.split(program)[0]) + +######################################################################## +# Configuration section + +## General configuration options +## + +# This is the IP address of the network interface you want your servers to +# be visible from. This can be changed to '' to listen on all interfaces. +IP_ADDRESS='' + +# IP address of your DNS server. Set to '' if you do not want to resolve +# IP addresses. If you have DNS service on your local machine then you can +# set this to '127.0.0.1' +DNS_IP='' + +# User id to run ZServer as. Note that this only works under Unix, and if +# ZServer is started by root. This no longer defaults to 'nobody' since +# that can lead to a Zope file compromise. +UID=None + +# Log file location. If this is a relative path, then it is joined the +# the 'var' directory. +LOG_FILE='Z2.log' + +## HTTP configuration +## + +# Port for HTTP Server. The standard port for HTTP services is 80. +HTTP_PORT=8080 + +# Port for HTTPS Server. The standard port for HTTPS services is 443. +HTTPS_PORT=8443 + +# HTTP enivornment settings. +HTTP_ENV={} + +# HTTPS enivornment settings. +HTTPS_ENV={} + +# Should we close all HTTP connections, ignoring the (usually absent) +# 'Connection:' header? +FORCE_HTTP_CONNECTION_CLOSE=0 + +# Port for the special "WebDAV source view" HTTP handler. There is no +# standard port for this handler, which is disabled by default. +WEBDAV_SOURCE_PORT=[] + +# Port for the special "WebDAV source view over SSL" HTTP handler. There is no +# standard port for this handler, which is disabled by default. +WEBDAV_SSL_SOURCE_PORT=[] + +# Should we use client X.509 certificate-based authentication? +X509_REMOTE_USER=None + +## FTP configuration + +# Port for the FTP Server. The standard port for FTP services is 21. +FTP_PORT=8021 + +## PCGI configuration + +# You can configure the PCGI server manually, or have it read its +# configuration information from a PCGI info file. +PCGI_FILE='Zope.cgi' + +## Monitor configuration +MONITOR_PORT=0 + +## ICP configuration +ICP_PORT=0 + +# Module to be published, which must be Main or Zope +MODULE='Zope' + +# The size of the thread pool, if ZODB3 is used. +NUMBER_OF_THREADS=4 + +# Localization support +LOCALE_ID=None + +# Socket path or port for the FastCGI Server +FCGI_PORT=None + +# Detailed log file +DETAILED_LOG_FILE='' + +# Use a daemon process +USE_DAEMON = 1 + +# +######################################################################## + +######################################################################## +# Handle command-line arguments: + +def server_info(old, v, offset=0): + # interpret v as a port or address/port and get new value + if v == '-': v='' + l=v.find(':') + if l >= 0: + a=v[:l] + v=v[l+1:] + else: + a=IP_ADDRESS + + if not v: return v + + try: + v=int(v) + if v < 0: raise 'Invalid port', v + v=v+offset + except: raise 'Invalid port', v + + if isinstance(old, IntType): old=[(a,v)] + else: old.append((a,v)) + + return old + + +try: + python_version = sys.version.split()[0] + if python_version < '2.1': + raise 'Invalid python version', python_version + if python_version[:3] == '2.1': + if python_version[4:5] < '3': + import warnings + err = ('You are running Python version %s. This Python version ' + 'has known bugs that may cause Zope to run improperly. ' + 'Consider upgrading to a Python in the 2.1 series ' + 'with at least version number 2.1.3. (Note that Zope does ' + 'not yet run under any Python 2.2 version).' % + python_version) + warnings.warn(err) + if python_version[:3] == '2.2': + import warnings + err = ('You are running Python version %s. This Python version ' + 'has not yet been tested with Zope and you may experience ' + 'operational problems as a result. Consider using ' + 'Python 2.1.3 instead.' % python_version) + warnings.warn(err) + + + opts, args = getopt.getopt(sys.argv[1:], + 'hz:Z:t:i:a:d:u:w:W:y:Y:x:f:p:m:Sl:2DP:rF:L:XM:C', + ['icp=', 'force-http-connection-close' + ]) + + DEBUG=0 + READ_ONLY=0 + if sys.platform == 'win32': + USE_DAEMON = 0 + + + # Get environment variables + for a in args: + if a.find('='): + a=a.split('=') + o=a[0] + v='='.join(a[1:]) + if o: + os.environ[o]=v + HTTP_ENV[o]=v + else: + raise 'Invalid argument', a + + for o, v in opts: + if o=='-z': here=v + elif o=='-Z': + if v in ('-', '0', ''): + USE_DAEMON=0 + elif sys.platform != 'win32': + USE_DAEMON = 1 + elif o=='-r': READ_ONLY=1 + elif o=='-t': + try: v=int(v) + except: raise 'Invalid number of threads', v + NUMBER_OF_THREADS=v + + elif o=='-i': + try: v=int(v) + except: raise 'Invalid value for -i option', v + sys.setcheckinterval(v) + + elif o=='-a': IP_ADDRESS=v + elif o=='-d': + if v=='-': v='' + DNS_IP=v + elif o=='-u': UID=v + elif o=='-D': + os.environ['Z_DEBUG_MODE']='1' + DEBUG=1 + elif o=='-S': sys.ZMANAGED=1 + elif o=='-X': + MONITOR_PORT=HTTP_PORT=FTP_PORT=FCGI_PORT=ICP_PORT=0 + WEBDAV_SOURCE_PORT=0 + PCGI_FILE='' + elif o=='-m': + MONITOR_PORT=server_info(MONITOR_PORT, v) + elif o=='-w': + HTTP_PORT=server_info(HTTP_PORT, v) + elif o=='-y': + HTTPS_PORT=server_info(HTTPS_PORT, v) + elif o=='-C' or o=='--force-http-connection-close': + FORCE_HTTP_CONNECTION_CLOSE=1 + elif o=='-W': + WEBDAV_SOURCE_PORT=server_info(WEBDAV_SOURCE_PORT, v) + elif o=='-Y': + WEBDAV_SSL_SOURCE_PORT=server_info(WEBDAV_SSL_SOURCE_PORT, v) + elif o=='-x': + if v in ('-', '0', ''): + X509_REMOTE_USER=None + else: + X509_REMOTE_USER=1 + elif o=='-f': + FTP_PORT=server_info(FTP_PORT, v) + elif o=='-P': + HTTP_PORT=server_info(HTTP_PORT, v, 80) + FTP_PORT=server_info(FTP_PORT, v, 21) + elif o=='--icp': + ICP_PORT=server_info(ICP_PORT, v) + + elif o=='-p': + if v=='-': v='' + PCGI_FILE=v + elif o=='-h': + print __doc__ % vars() + sys.exit(0) + elif o=='-2': MODULE='Main' + elif o=='-l': LOG_FILE=v + elif o=='-L': + if v: LOCALE_ID=v + else: LOCALE_ID='' + elif o=='-F': + if v=='-': v='' + FCGI_PORT=v + elif o=='-M': DETAILED_LOG_FILE=v + +except SystemExit: sys.exit(0) +except: + print __doc__ % vars() + print + print 'Error:' + print "%s: %s" % (sys.exc_type, sys.exc_value) + sys.exit(1) + +# +######################################################################## + +######################################################################## +# OK, let's get going! + +# Jigger path: +sys.path=[os.path.join(here,'lib','python'),here + ]+filter(None, sys.path) + + + +# Try to set the locale if specified on the command +# line. If the locale module is not available or the +# requested locale is not supported by the local +# machine, raise an error so that the user is made +# aware of the problem. + +def set_locale(val): + try: + import locale + except: + raise SystemExit, ( + 'The locale module could not be imported.\n' + 'To use localization options, you must ensure\n' + 'that the locale module is compiled into your\n' + 'Python installation.' + ) + try: + locale.setlocale(locale.LC_ALL, val) + except: + raise SystemExit, ( + 'The specified locale is not supported by your system.\n' + 'See your operating system documentation for more\n' + 'information on locale support.' + ) +if LOCALE_ID is not None: + set_locale(LOCALE_ID) + +import zdaemon +# from this point forward we can use the zope logger +# importing ZDaemon before importing ZServer causes ZServer logging +# not to work. + +# Import ZServer before we open the database or get at interesting +# application code so that ZServer's asyncore gets to be the +# official one. Also gets SOFTWARE_HOME, INSTANCE_HOME, and CLIENT_HOME +import ZServer + +# install signal handlers if on posix +if os.name == 'posix': + from Signals import Signals + Signals.registerZopeSignals() + +# Location of the ZServer pid file. When Zope starts up it will write +# its PID to this file. If Zope is run under zdaemon control, zdaemon +# will write to this pidfile instead of Zope. +PID_FILE=os.path.join(CLIENT_HOME, 'Z2.pid') + +if USE_DAEMON and not READ_ONLY: + import App.FindHomes + sys.ZMANAGED=1 + # zdaemon.run creates a process which "manages" the actual Zope + # process (restarts it if it dies). The management process passes along + # signals that it receives to its child. + zdaemon.run(sys.argv, os.path.join(CLIENT_HOME, PID_FILE)) + +os.chdir(CLIENT_HOME) + +def _warn_nobody(): + zLOG.LOG("z2", zLOG.INFO, ("Running Zope as 'nobody' can compromise " + "your Zope files; consider using a " + "dedicated user account for Zope") ) + +try: + # Import logging support + import zLOG + import ZLogger + + if READ_ONLY: + if hasattr(zLOG, '_set_stupid_dest'): + zLOG._set_stupid_dest(sys.stderr) + else: + zLOG._stupid_dest = sys.stderr + else: + zLOG.log_write = ZLogger.ZLogger.log_write + + if DETAILED_LOG_FILE: + from ZServer import DebugLogger + logfile=os.path.join(CLIENT_HOME, DETAILED_LOG_FILE) + zLOG.LOG('z2', zLOG.BLATHER, + 'Using detailed request log file %s' % logfile) + DL=DebugLogger.DebugLogger(logfile) + DebugLogger.log=DL.log + DebugLogger.reopen=DL.reopen + sys.__detailedlog=DL + + # Import Zope (or Main) + if MODULE == 'Zope': + import Zope + Zope.startup() + else: + exec "import "+MODULE in {} + + # Location of the ZServer log file. This file logs all ZServer activity. + # You may wish to create different logs for different servers. See + # medusa/logger.py for more information. + if not os.path.isabs(LOG_FILE): + LOG_PATH=os.path.join(CLIENT_HOME, LOG_FILE) + else: + LOG_PATH=LOG_FILE + + # import ZServer stuff + + # First, we need to increase the number of threads + if MODULE=='Zope': + from ZServer import setNumberOfThreads + setNumberOfThreads(NUMBER_OF_THREADS) + + from ZServer import resolver, logger, asyncore + + from ZServer import zhttp_server, zhttp_handler + from ZServer import zhttps_server, zhttps0_handler, zhttps_handler + from ZServer.WebDAVSrcHandler import WebDAVSrcHandler + from ZServer import PCGIServer,FTPServer,FCGIServer + + from ZServer import secure_monitor_server + + from M2Crypto import SSL, Rand + + ## ZServer startup + ## + + ## In X509_REMOTE_USER mode, we log the client cert's subject DN. + if X509_REMOTE_USER: + + import base64, string, time + + def log (self, bytes): + user_agent=self.get_header('user-agent') + if not user_agent: user_agent='' + referer=self.get_header('referer') + if not referer: referer='' + + get_peer_cert = getattr(self.channel, 'get_peer_cert', None) + if get_peer_cert is not None: + name = str(get_peer_cert().get_subject()) + else: + name = 'Anonymous' + auth=self.get_header('Authorization') + if auth is not None: + if string.lower(auth[:6]) == 'basic ': + try: decoded=base64.decodestring(auth[6:]) + except base64.binascii.Error: decoded='' + t = string.split(decoded, ':', 1) + if len(t) < 2: + name = 'Unknown (bad auth string)' + else: + name = t[0] + + self.channel.server.logger.log ( + self.channel.addr[0], + ' - %s [%s] "%s" %d %d "%s" "%s"\n' % ( + name, + self.log_date_string (time.time()), + self.request, + self.reply_code, + bytes, + referer, + user_agent + ) + ) + + from ZServer.medusa import http_server + http_server.http_request.log = log + + # Resolver and Logger, used by other servers + if DNS_IP: + rs = resolver.caching_resolver(DNS_IP) + else: + rs=None + + if READ_ONLY: + lg = logger.file_logger('-') # log to stdout + zLOG.LOG('z2', zLOG.BLATHER, 'Logging access log to stdout') + elif os.environ.has_key('ZSYSLOG_ACCESS'): + if os.environ.has_key("ZSYSLOG_ACCESS_FACILITY"): + lg = logger.syslog_logger( + os.environ['ZSYSLOG_ACCESS'], + facility=os.environ['ZSYSLOG_ACCESS_FACILITY']) + else: + lg = logger.syslog_logger(os.environ['ZSYSLOG_ACCESS']) + zLOG.LOG('z2', zLOG.BLATHER, 'Using local syslog access log') + elif os.environ.has_key('ZSYSLOG_ACCESS_SERVER'): + (addr, port) = os.environ['ZSYSLOG_ACCESS_SERVER'].split( ':') + lg = logger.syslog_logger((addr, int(port))) + zLOG.LOG('z2', zLOG.BLATHER, 'Using remote syslog access log') + else: + lg = logger.file_logger(LOG_PATH) + zLOG.LOG('z2', zLOG.BLATHER, 'Using access log file %s' % LOG_PATH) + sys.__lg = lg + + port_err=('\n\nZope wants to use %(socktype)s port %(port)s for its ' + '%(protocol)s service, but it is already in use by another ' + 'application on this machine. Either shut the application down ' + 'which is using this port, or start Zope with a different ' + '%(protocol)s port via the "%(switch)s" command-line switch.\n') + + # HTTP Server + if HTTP_PORT: + if isinstance(HTTP_PORT, IntType): HTTP_PORT=((IP_ADDRESS, HTTP_PORT),) + for address, port in HTTP_PORT: + try: + hs = zhttp_server( + ip=address, + port=port, + resolver=rs, + logger_object=lg) + except socket.error, why: + if why[0] == 98: # address in use + raise port_err % {'port':port, + 'socktype':'TCP', + 'protocol':'HTTP', + 'switch':'-w'} + raise + # Handler for a published module. zhttp_handler takes 3 arguments: + # The name of the module to publish, and optionally the URI base + # which is basically the SCRIPT_NAME, and optionally a dictionary + # with CGI environment variables which override default + # settings. The URI base setting is useful when you want to + # publish more than one module with the same HTTP server. The CGI + # environment setting is useful when you want to proxy requests + # from another web server to ZServer, and would like the CGI + # environment to reflect the CGI environment of the other web + # server. + try: + del HTTP_ENV['HTTPS'] + except KeyError: + pass + zh = zhttp_handler(MODULE, '', HTTP_ENV) + if FORCE_HTTP_CONNECTION_CLOSE: + zh._force_connection_close = 1 + hs.install_handler(zh) + + # HTTPS Server + if HTTPS_PORT: + ssl_ctx = SSL.Context('sslv23') + ssl_ctx.load_cert_chain('%s/server.pem' % INSTANCE_HOME) + ssl_ctx.load_verify_locations('%s/ca.pem' % INSTANCE_HOME) + ssl_ctx.load_client_CA('%s/ca.pem' % INSTANCE_HOME) + #ssl_ctx.set_allow_unknown_ca(1) + ssl_ctx.set_session_id_ctx(MODULE) + ssl_ctx.set_tmp_dh('%s/dh1024.pem' % INSTANCE_HOME) + if X509_REMOTE_USER: + ssl_ctx.set_verify(SSL.verify_peer, 10) + else: + ssl_ctx.set_verify(SSL.verify_none, 10) + if type(HTTPS_PORT) is type(0): HTTPS_PORT=((IP_ADDRESS, HTTPS_PORT),) + + for address, port in HTTPS_PORT: + hss = zhttps_server( + ip=address, + port=port, + ssl_ctx=ssl_ctx, + resolver=rs, + logger_object=lg) + + try: + del HTTPS_ENV['HTTP'] + except KeyError: + pass + HTTPS_ENV['HTTPS']='ON' + + if X509_REMOTE_USER: + zsh = zhttps_handler(MODULE, '', HTTPS_ENV) + else: + zsh = zhttps0_handler(MODULE, '', HTTPS_ENV) + hss.install_handler(zsh) + + # WebDAV source Server (runs HTTP, but munges request to return + # 'manage_FTPget'). + if WEBDAV_SOURCE_PORT: + if isinstance(WEBDAV_SOURCE_PORT, IntType): + WEBDAV_SOURCE_PORT=((IP_ADDRESS, WEBDAV_SOURCE_PORT),) + for address, port in WEBDAV_SOURCE_PORT: + try: + hs = zhttp_server( + ip=address, + port=port, + resolver=rs, + logger_object=lg) + except socket.error, why: + if why[0] == 98: # address in use + raise port_err % {'port':port, + 'socktype':'TCP', + 'protocol':'WebDAV source', + 'switch':'-W'} + raise + + # Handler for a published module. zhttp_handler takes 3 arguments: + # The name of the module to publish, and optionally the URI base + # which is basically the SCRIPT_NAME, and optionally a dictionary + # with CGI environment variables which override default + # settings. The URI base setting is useful when you want to + # publish more than one module with the same HTTP server. The CGI + # environment setting is useful when you want to proxy requests + # from another web server to ZServer, and would like the CGI + # environment to reflect the CGI environment of the other web + # server. + zh = WebDAVSrcHandler(MODULE, '', HTTP_ENV) + hs.install_handler(zh) + + # enable document retrieval of the document source on the + # standard HTTP port + + clients = os.environ.get('WEBDAV_SOURCE_PORT_CLIENTS') + if clients: + import re + sys.WEBDAV_SOURCE_PORT_CLIENTS = re.compile(clients).search + else: + sys.WEBDAV_SOURCE_PORT_CLIENTS = None + + # WebDAV-over-SSL source Server (runs HTTPS, but munges request to return + # 'manage_FTPget'). + if WEBDAV_SSL_SOURCE_PORT: + ssl_ctx = SSL.Context('sslv23') + ssl_ctx.load_cert_chain('%s/server.pem' % INSTANCE_HOME) + ssl_ctx.load_verify_locations('%s/ca.pem' % INSTANCE_HOME) + ssl_ctx.load_client_CA('%s/ca.pem' % INSTANCE_HOME) + ssl_ctx.set_verify(SSL.verify_none, 10) + ssl_ctx.set_session_id_ctx(MODULE) + ssl_ctx.set_tmp_dh('%s/dh1024.pem' % INSTANCE_HOME) + if type(WEBDAV_SSL_SOURCE_PORT) is type(0): + WEBDAV_SSL_SOURCE_PORT=((IP_ADDRESS, WEBDAV_SSL_SOURCE_PORT),) + for address, port in WEBDAV_SSL_SOURCE_PORT: + hss = zhttps_server( + ip=address, + port=port, + ssl_ctx=ssl_ctx, + resolver=rs, + logger_object=lg) + + try: + del HTTPS_ENV['HTTP'] + except KeyError: + pass + HTTPS_ENV['HTTPS']='ON' + + zsh = WebDAVSrcHandler(MODULE, '', HTTPS_ENV) + hss.install_handler(zsh) + + # FTP Server + if FTP_PORT: + if isinstance(FTP_PORT, IntType): FTP_PORT=((IP_ADDRESS, FTP_PORT),) + for address, port in FTP_PORT: + try: + FTPServer( + module=MODULE, + ip=address, + port=port, + resolver=rs, + logger_object=lg) + except socket.error, why: + if why[0] == 98: # address in use + raise port_err % {'port':port, + 'socktype':'TCP', + 'protocol':'FTP', + 'switch':'-f'} + raise + + # PCGI Server + if PCGI_FILE and not READ_ONLY: + PCGI_FILE=os.path.join(here, PCGI_FILE) + if os.path.exists(PCGI_FILE): + zpcgi = PCGIServer( + module=MODULE, + ip=IP_ADDRESS, + pcgi_file=PCGI_FILE, + resolver=rs, + logger_object=lg) + + + # FastCGI Server + if FCGI_PORT and not READ_ONLY: + fcgiPort = None + fcgiPath = None + try: + fcgiPort = int(FCGI_PORT) + except ValueError: + fcgiPath = FCGI_PORT + try: + zfcgi = FCGIServer(module=MODULE, + ip=IP_ADDRESS, + port=fcgiPort, + socket_file=fcgiPath, + resolver=rs, + logger_object=lg) + except socket.error, why: + if why[0] == 98: # address in use + raise port_err % {'port':fcgiPort, + 'socktype':'TCP', + 'protocol':'FastCGI', + 'switch':'-F'} + raise + + + # Monitor Server + if MONITOR_PORT: + from AccessControl.User import emergency_user + if not hasattr(emergency_user, '__null_user__'): + pw = emergency_user._getPassword() + else: + pw = None + zLOG.LOG("z2", zLOG.WARNING, 'Monitor server not started' + ' because no emergency user exists.') + if pw: + if isinstance(MONITOR_PORT, IntType): + MONITOR_PORT=((IP_ADDRESS, MONITOR_PORT),) + for address, port in MONITOR_PORT: + try: + monitor=secure_monitor_server( + password=pw, + hostname=address, + port=port) + except socket.error, why: + if why[0] == 98: # address in use + raise port_err % {'port':port, + 'socktype':'TCP', + 'protocol':'monitor server', + 'switch':'-m'} + raise + + if ICP_PORT: + if isinstance(ICP_PORT, IntType): ICP_PORT=((IP_ADDRESS, ICP_PORT),) + from ZServer.ICPServer import ICPServer + for address, port in ICP_PORT: + try: + ICPServer(address,port) + except socket.error, why: + if why[0] == 98: # address in use + raise port_err % {'port':port, + 'socktype':'UDP', + 'protocol':'ICP', + 'switch':'--icp'} + raise + + if not USE_DAEMON and not READ_ONLY: + if os.path.exists(PID_FILE): os.unlink(PID_FILE) + pf = open(PID_FILE, 'w') + pid='%s\n' % os.getpid() + pf.write(pid) + pf.close() + + # Warn if we were started as nobody. + try: + import pwd + if os.getuid(): + if pwd.getpwuid(os.getuid())[0] == 'nobody': + _warn_nobody() + except: + pass + + # Drop root privileges if we have them, and do some sanity checking + # to make sure we're not starting with an obviously insecure setup. + try: + if os.getuid() == 0: + try: + import initgroups + except: + raise SystemExit, 'initgroups is required to safely setuid' + if UID == None: + raise SystemExit, ('A user was not specified to setuid ' + 'to; fix this to start as root (see ' + 'doc/SETUID.txt)') + import stat + client_home_stat = os.stat(CLIENT_HOME) + client_home_faults = [] + if not (client_home_stat[stat.ST_MODE]&01000): + client_home_faults.append('does not have the sticky bit set') + if client_home_stat[stat.ST_UID] != 0: + client_home_faults.append('is not owned by root') + if client_home_faults: + client_home_faults.append('fix this to start as root (see ' + 'doc/SETUID.txt)') + err = '%s %s' % (CLIENT_HOME, ', '.join(client_home_faults)) + raise SystemExit, err + + try: + try: UID = string.atoi(UID) + except: pass + gid = None + if isinstance(UID, StringType): + uid = pwd.getpwnam(UID)[2] + gid = pwd.getpwnam(UID)[3] + elif isinstance(UID, IntType): + uid = pwd.getpwuid(UID)[2] + gid = pwd.getpwuid(UID)[3] + UID = pwd.getpwuid(UID)[0] + else: + raise KeyError + if UID == 'nobody': + _warn_nobody() + try: + initgroups.initgroups(UID, gid) + if gid is not None: + try: + os.setgid(gid) + except OSError: + pass + os.setuid(uid) + except OSError: + pass + except KeyError: + zLOG.LOG("z2", zLOG.ERROR, ("Can't find UID %s" % UID)) + except AttributeError: + pass + except: + raise + + # Check umask sanity if we're on posix. + if os.name == 'posix' and not os.environ.get('Z_DEBUG_MODE'): + # umask is silly, blame POSIX. We have to set it to get its value. + current_umask = os.umask(0) + os.umask(current_umask) + if current_umask != 077: + current_umask = '%03o' % current_umask + zLOG.LOG("z2", zLOG.INFO, ( + 'Your umask of %s may be too permissive; for the security of ' + 'your Zope data, it is recommended you use 077' % current_umask + )) + +except: + # Log startup exception and tell zdaemon not to restart us. + try: + zLOG.LOG("z2", zLOG.PANIC, "Startup exception", + error=sys.exc_info()) + except: pass + sys.exit(0) + +# Start Medusa, Ye Hass! +Rand.load_file('%s/randpool.dat' % INSTANCE_HOME, -1) +sys.ZServerExitCode=0 +asyncore.loop() +Rand.save_file('%s/randpool.dat' % INSTANCE_HOME) +sys.exit(sys.ZServerExitCode) diff --git a/demo/Zope/z2s.py.diff b/demo/Zope/z2s.py.diff new file mode 100644 index 0000000..f79cd94 --- /dev/null +++ b/demo/Zope/z2s.py.diff @@ -0,0 +1,266 @@ +--- z2s.py Sun Oct 26 17:51:00 2003 ++++ /usr/local/home/ngps/pkg/zope261/z2.py Thu Jan 30 22:41:42 2003 +@@ -105,21 +105,9 @@ + + Multiple -w options can be provided to run multiple servers. + +- -y port +- +- The SSL Web server (HTTPS) port. This defaults to %(HTTPS_PORT)s. The +- standard port for HTTPS services is 443. If this is a dash +- (e.g. -y -), then HTTPS is disabled. +- +- The number can be preeceeded by an ip address follwed by a colon +- to specify an address to listen on. This allows different servers +- to listen on different addresses. +- +- Multiple -y options can be provided to run multiple servers. +- + -W port + +- The "WebDAV source" port. If this is a dash (e.g. -W -), then ++ The "WebDAV source" port. If this is a dash (e.g. -w -), then + "WebDAV source" is disabled. The default is disabled. Note that + this feature is a workaround for the lack of "source-link" support + in standard WebDAV clients. +@@ -130,24 +118,6 @@ + + Multiple -W options can be provided to run multiple servers. + +- -Y port +- +- The "WebDAV source over HTTPS" port. If this is a dash (e.g. -Y -), then +- "WebDAV source over HTTPS" is disabled. The default is disabled. Note that +- this feature is a workaround for the lack of "source-link" support +- in standard WebDAV clients. +- +- The port can be preeceeded by an ip address follwed by a colon +- to specify an address to listen on. This allows different servers +- to listen on different addresses. +- +- Multiple -Y options can be provided to run multiple servers. +- +- -x +- +- If present, this option causes Zope to run in X.509 certificate-based +- authentication mode. +- + -C + --force-http-connection-close + +@@ -316,15 +286,9 @@ + # Port for HTTP Server. The standard port for HTTP services is 80. + HTTP_PORT=8080 + +-# Port for HTTPS Server. The standard port for HTTPS services is 443. +-HTTPS_PORT=8443 +- + # HTTP enivornment settings. + HTTP_ENV={} + +-# HTTPS enivornment settings. +-HTTPS_ENV={} +- + # Should we close all HTTP connections, ignoring the (usually absent) + # 'Connection:' header? + FORCE_HTTP_CONNECTION_CLOSE=0 +@@ -333,13 +297,6 @@ + # standard port for this handler, which is disabled by default. + WEBDAV_SOURCE_PORT=[] + +-# Port for the special "WebDAV source view over SSL" HTTP handler. There is no +-# standard port for this handler, which is disabled by default. +-WEBDAV_SSL_SOURCE_PORT=[] +- +-# Should we use client X.509 certificate-based authentication? +-X509_REMOTE_USER=None +- + ## FTP configuration + + # Port for the FTP Server. The standard port for FTP services is 21. +@@ -429,7 +386,7 @@ + + + opts, args = getopt.getopt(sys.argv[1:], +- 'hz:Z:t:i:a:d:u:w:W:y:Y:x:f:p:m:Sl:2DP:rF:L:XM:C', ++ 'hz:Z:t:i:a:d:u:w:W:f:p:m:Sl:2DP:rF:L:XM:C', + ['icp=', 'force-http-connection-close' + ]) + +@@ -486,19 +443,10 @@ + MONITOR_PORT=server_info(MONITOR_PORT, v) + elif o=='-w': + HTTP_PORT=server_info(HTTP_PORT, v) +- elif o=='-y': +- HTTPS_PORT=server_info(HTTPS_PORT, v) + elif o=='-C' or o=='--force-http-connection-close': + FORCE_HTTP_CONNECTION_CLOSE=1 + elif o=='-W': + WEBDAV_SOURCE_PORT=server_info(WEBDAV_SOURCE_PORT, v) +- elif o=='-Y': +- WEBDAV_SSL_SOURCE_PORT=server_info(WEBDAV_SSL_SOURCE_PORT, v) +- elif o=='-x': +- if v in ('-', '0', ''): +- X509_REMOTE_USER=None +- else: +- X509_REMOTE_USER=1 + elif o=='-f': + FTP_PORT=server_info(FTP_PORT, v) + elif o=='-P': +@@ -653,60 +601,14 @@ + from ZServer import resolver, logger, asyncore + + from ZServer import zhttp_server, zhttp_handler +- from ZServer import zhttps_server, zhttps0_handler, zhttps_handler + from ZServer.WebDAVSrcHandler import WebDAVSrcHandler + from ZServer import PCGIServer,FTPServer,FCGIServer + + from ZServer import secure_monitor_server + +- from M2Crypto import SSL, Rand +- + ## ZServer startup + ## + +- ## In X509_REMOTE_USER mode, we log the client cert's subject DN. +- if X509_REMOTE_USER: +- +- import base64, string, time +- +- def log (self, bytes): +- user_agent=self.get_header('user-agent') +- if not user_agent: user_agent='' +- referer=self.get_header('referer') +- if not referer: referer='' +- +- get_peer_cert = getattr(self.channel, 'get_peer_cert', None) +- if get_peer_cert is not None: +- name = str(get_peer_cert().get_subject()) +- else: +- name = 'Anonymous' +- auth=self.get_header('Authorization') +- if auth is not None: +- if string.lower(auth[:6]) == 'basic ': +- try: decoded=base64.decodestring(auth[6:]) +- except base64.binascii.Error: decoded='' +- t = string.split(decoded, ':', 1) +- if len(t) < 2: +- name = 'Unknown (bad auth string)' +- else: +- name = t[0] +- +- self.channel.server.logger.log ( +- self.channel.addr[0], +- ' - %s [%s] "%s" %d %d "%s" "%s"\n' % ( +- name, +- self.log_date_string (time.time()), +- self.request, +- self.reply_code, +- bytes, +- referer, +- user_agent +- ) +- ) +- +- from ZServer.medusa import http_server +- http_server.http_request.log = log +- + # Resolver and Logger, used by other servers + if DNS_IP: + rs = resolver.caching_resolver(DNS_IP) +@@ -766,51 +668,11 @@ + # from another web server to ZServer, and would like the CGI + # environment to reflect the CGI environment of the other web + # server. +- try: +- del HTTP_ENV['HTTPS'] +- except KeyError: +- pass + zh = zhttp_handler(MODULE, '', HTTP_ENV) + if FORCE_HTTP_CONNECTION_CLOSE: + zh._force_connection_close = 1 + hs.install_handler(zh) + +- # HTTPS Server +- if HTTPS_PORT: +- ssl_ctx = SSL.Context('sslv23') +- ssl_ctx.load_cert_chain('%s/server.pem' % INSTANCE_HOME) +- ssl_ctx.load_verify_locations('%s/ca.pem' % INSTANCE_HOME) +- ssl_ctx.load_client_CA('%s/ca.pem' % INSTANCE_HOME) +- #ssl_ctx.set_allow_unknown_ca(1) +- ssl_ctx.set_session_id_ctx(MODULE) +- ssl_ctx.set_tmp_dh('%s/dh1024.pem' % INSTANCE_HOME) +- if X509_REMOTE_USER: +- ssl_ctx.set_verify(SSL.verify_peer|SSL.verify_fail_if_no_peer_cert, 10) +- #ssl_ctx.set_verify(SSL.verify_peer, 10) +- else: +- ssl_ctx.set_verify(SSL.verify_none, 10) +- if type(HTTPS_PORT) is type(0): HTTPS_PORT=((IP_ADDRESS, HTTPS_PORT),) +- +- for address, port in HTTPS_PORT: +- hss = zhttps_server( +- ip=address, +- port=port, +- ssl_ctx=ssl_ctx, +- resolver=rs, +- logger_object=lg) +- +- try: +- del HTTPS_ENV['HTTP'] +- except KeyError: +- pass +- HTTPS_ENV['HTTPS']='ON' +- +- if X509_REMOTE_USER: +- zsh = zhttps_handler(MODULE, '', HTTPS_ENV) +- else: +- zsh = zhttps0_handler(MODULE, '', HTTPS_ENV) +- hss.install_handler(zsh) +- + # WebDAV source Server (runs HTTP, but munges request to return + # 'manage_FTPget'). + if WEBDAV_SOURCE_PORT: +@@ -854,34 +716,6 @@ + else: + sys.WEBDAV_SOURCE_PORT_CLIENTS = None + +- # WebDAV-over-SSL source Server (runs HTTPS, but munges request to return +- # 'manage_FTPget'). +- if WEBDAV_SSL_SOURCE_PORT: +- ssl_ctx = SSL.Context('sslv23') +- ssl_ctx.load_cert_chain('%s/server.pem' % INSTANCE_HOME) +- ssl_ctx.load_verify_locations('%s/ca.pem' % INSTANCE_HOME) +- ssl_ctx.load_client_CA('%s/ca.pem' % INSTANCE_HOME) +- ssl_ctx.set_verify(SSL.verify_none, 10) +- ssl_ctx.set_session_id_ctx(MODULE) +- ssl_ctx.set_tmp_dh('%s/dh1024.pem' % INSTANCE_HOME) +- if type(WEBDAV_SSL_SOURCE_PORT) is type(0): +- WEBDAV_SSL_SOURCE_PORT=((IP_ADDRESS, WEBDAV_SSL_SOURCE_PORT),) +- for address, port in WEBDAV_SSL_SOURCE_PORT: +- hss = zhttps_server( +- ip=address, +- port=port, +- ssl_ctx=ssl_ctx, +- resolver=rs, +- logger_object=lg) +- +- try: +- del HTTPS_ENV['HTTP'] +- except KeyError: +- pass +- HTTPS_ENV['HTTPS']='ON' +- +- zsh = WebDAVSrcHandler(MODULE, '', HTTPS_ENV) +- hss.install_handler(zsh) + + # FTP Server + if FTP_PORT: +@@ -1072,8 +906,6 @@ + sys.exit(0) + + # Start Medusa, Ye Hass! +-Rand.load_file('%s/randpool.dat' % INSTANCE_HOME, -1) + sys.ZServerExitCode=0 + asyncore.loop() +-Rand.save_file('%s/randpool.dat' % INSTANCE_HOME) + sys.exit(sys.ZServerExitCode) diff --git a/demo/Zope27/INSTALL.txt b/demo/Zope27/INSTALL.txt new file mode 100644 index 0000000..b3728c5 --- /dev/null +++ b/demo/Zope27/INSTALL.txt @@ -0,0 +1,200 @@ +========================================= + ZServerSSL for Zope 2.7.0b2 +========================================= + +:Author: Ng Pheng Siong +:Id: $Id$ +:Date: $Date$ +:Web-Site: http://sandbox.rulemaker.net/ngps/zope/zssl/ + +.. contents:: + + +Directories +----------------------- + +This distribution is contained in ``m2crypto-0.12/demo/Zope27/``. Its +directory structure assumes the following: + +- Zope 2.7.0b2 is installed in <install-dir>. +- An instance has been created in <instance-home>. + + +<install-dir> +----------------------- + +The following files are to be copied to the corresponding directories +in your <install-dir>: + +- install_dir/lib/python/ZServer/HTTPS_Server.py +- install_dir/lib/python/ZServer/medusa/https_server.py + +The following patch files are to be applied to the corresponding +directories in your <install-dir>: + +- install_dir/lib/python/ZServer/__init__.py.patch +- install_dir/lib/python/ZServer/component.xml.patch +- install_dir/lib/python/ZServer/datatypes.py.patch + + +<instance-home> +----------------------- + +The following files are to be copied to the corresponding directories +in your <instance-home>: + +- instance_home/ssl/ca.pem +- instance_home/ssl/server.pem +- instance_home/ssl/dh1024.pem + +These are example files. For more information on them, consult the +ZServerSSL HOWTO for Zope 2.6. + +The following patch files are to be applied to the corresponding +directories in your <instance-home>: + +- instance_home/README.txt.patch +- instance_home/etc/zope.conf.patch + +(Patching README.txt is optional.) + +There appears to be a bug in Zope 2.7.0b2, where INSTANCE_HOME in a +running Zope points to <install-dir>, not <instance-home>. Workaround +this with the following: + +:: + + $ (cd <install-dir>; ln -s <instance-home>/ssl) + + +Launch ZServerSSL +------------------- + +:: + + $ <instance-home>/bin/runzope + + +Testing +--------- + +Below, we assume your Zope server is running on ``localhost``. + + +HTTPS +~~~~~~~ + +This testing is done with Mozilla 1.1 on FreeBSD. + +1. With a browser, connect to https://localhost:8443/. Browse + around. Check out your browser's HTTPS informational screens. + +2. Connect to https://localhost:8443/manage. Verify that you can + access Zope's management functionality. + + +WebDAV-over-HTTPS +~~~~~~~~~~~~~~~~~~~ + +This testing is done with Cadaver 0.21.0 on FreeBSD. + +:: + + $ cadaver https://localhost:8443/ + WARNING: Untrusted server certificate presented: + Issued to: M2Crypto, SG + Issued by: M2Crypto, SG + Do you wish to accept the certificate? (y/n) y + Authentication required for Zope on server `localhost': + Username: admin + Password: + dav:/> ls + Listing collection `/': succeeded. + Coll: Control_Panel 0 Sep 28 00:38 + Coll: temp_folder 0 Sep 28 17:30 + acl_users 0 Sep 28 00:38 + browser_id_manager 0 Sep 28 00:38 + error_log 0 Sep 28 00:38 + index_html 28 Sep 28 00:39 + session_data_manager 0 Sep 28 00:38 + standard_error_message 1189 Sep 28 00:39 + standard_html_footer 18 Sep 28 00:39 + standard_html_header 82 Sep 28 00:39 + standard_template.pt 282 Sep 28 00:39 + dav:/> quit + Connection to `localhost' closed. + $ + + +Python with M2Crypto +~~~~~~~~~~~~~~~~~~~~~~ + +This testing is done with M2Crypto 0.12 and Python 2.2.3 on FreeBSD. + +HTTPS +``````` + +>>> from M2Crypto import Rand, SSL, m2urllib +>>> url = m2urllib.FancyURLopener() +>>> url.addheader('Connection', 'close') +>>> u = url.open('https://127.0.0.1:8443/') +send: 'GET / HTTP/1.1\r\nHost: 127.0.0.1:8443\r\nAccept-Encoding: identity\r\nUser-agent: Python-urllib/1.15\r\nConnection: close\r\n\r\n' +reply: 'HTTP/1.1 200 OK\r\n' +header: Server: ZServerSSL/0.12 +header: Date: Sun, 28 Sep 2003 09:40:14 GMT +header: Content-Length: 3055 +header: Etag: +header: Content-Type: text/html +header: Connection: close +>>> while 1: +... data = u.read() +... if not data: break +... print data +... + +:: + + [blah blah blah] + + <p> + Go directly to the <a href="/manage" target="_top"> + Zope Management Interface</a> if you'd like to start working with Zope + right away. <strong>NOTE: Some versions of Microsoft Internet Explorer, + (specifically IE 5.01 and early versions of IE 5.5) may have problems + displaying Zope management pages. If you cannot view the management pages, + try upgrading your IE installation to the latest release version, or use + a different browser.</strong> + </p> + </li> + + <li> + <p> + Find out about <a href="http://www.zope.com/" target="_new">Zope + Corporation</a>, the publishers of Zope. + </p> + </li> + + </ul> + + </body> + </html> + +>>> u.close() +>>> + + +XMLRPC-over-HTTPS +``````````````````` + +>>> from M2Crypto.m2xmlrpclib import Server, SSL_Transport +>>> zs = Server('https://127.0.0.1:8443/', SSL_Transport()) +>>> print zs.propertyMap() +[{'type': 'string', 'id': 'title', 'mode': 'w'}] +>>> + + +Conclusion +------------ + +Yes, it works! ;-) + diff --git a/demo/Zope27/install_dir/lib/python/ZServer/HTTPS_Server.py b/demo/Zope27/install_dir/lib/python/ZServer/HTTPS_Server.py new file mode 100644 index 0000000..49b6177 --- /dev/null +++ b/demo/Zope27/install_dir/lib/python/ZServer/HTTPS_Server.py @@ -0,0 +1,187 @@ +############################################################################## +# +# Copyright (c) 2000-2003, Ng Pheng Siong. All Rights Reserved. +# This file is derived from Zope's ZServer/HTTPServer.py. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE +# +############################################################################## + +""" +Medusa HTTPS server for Zope + +changes from Medusa's http_server: + + Request Threads -- Requests are processed by threads from a thread + pool. + + Output Handling -- Output is pushed directly into the producer + fifo by the request-handling thread. The HTTP server does not do + any post-processing such as chunking. + + Pipelineable -- This is needed for protocols such as HTTP/1.1 in + which mutiple requests come in on the same channel, before + responses are sent back. When requests are pipelined, the client + doesn't wait for the response before sending another request. The + server must ensure that responses are sent back in the same order + as requests are received. + + +changes from Zope's HTTP server: + + Well, this is a *HTTPS* server :) + + X.509 certificate-based authentication -- When this is in force, + zhttps_handler, a subclass of zhttp_handler, is installed. The + https server is configured to request an X.509 certificate from + the client. When the request reaches zhttps_handler, it sets + REMOTE_USER to the client's subject distinguished name (DN) from + the certificate. Zope's REMOTE_USER machinery takes care of the + rest, e.g., in conjunction with the RemoteUserFolder product. + +""" + +import sys, time, types + +from PubCore import handle +import asyncore +from ZServer import CONNECTION_LIMIT, ZOPE_VERSION +from HTTPServer import zhttp_handler +from zLOG import register_subsystem + +from M2Crypto import SSL +from medusa.https_server import https_server, https_channel +from asyncore import dispatcher + + +ZSERVER_SSL_VERSION='0.12' + +register_subsystem('ZServer HTTPS_Server') + + +class zhttps0_handler(zhttp_handler): + "zhttps0 handler - sets SSL request headers a la mod_ssl" + + def __init__ (self, module, uri_base=None, env=None): + zhttp_handler.__init__(self, module, uri_base, env) + + def get_environment(self, request): + env = zhttp_handler.get_environment(self, request) + env['SSL_CIPHER'] = request.channel.get_cipher() + return env + + +class zhttps_handler(zhttps0_handler): + "zhttps handler - sets REMOTE_USER to user's X.509 certificate Subject DN" + + def __init__ (self, module, uri_base=None, env=None): + zhttps0_handler.__init__(self, module, uri_base, env) + + def get_environment(self, request): + env = zhttps0_handler.get_environment(self, request) + peer = request.channel.get_peer_cert() + if peer is not None: + env['REMOTE_USER'] = str(peer.get_subject()) + return env + + +class zhttps_channel(https_channel): + "https channel" + + closed=0 + zombie_timeout=100*60 # 100 minutes + + def __init__(self, server, conn, addr): + https_channel.__init__(self, server, conn, addr) + self.queue=[] + self.working=0 + self.peer_found=0 + + def push(self, producer, send=1): + # this is thread-safe when send is false + # note, that strings are not wrapped in + # producers by default + if self.closed: + return + self.producer_fifo.push(producer) + if send: self.initiate_send() + + push_with_producer=push + + def work(self): + "try to handle a request" + if not self.working: + if self.queue: + self.working=1 + try: module_name, request, response=self.queue.pop(0) + except: return + handle(module_name, request, response) + + def close(self): + self.closed=1 + while self.queue: + self.queue.pop() + if self.current_request is not None: + self.current_request.channel=None # break circ refs + self.current_request=None + while self.producer_fifo: + p=self.producer_fifo.first() + if p is not None and type(p) != types.StringType: + p.more() # free up resources held by producer + self.producer_fifo.pop() + self.del_channel() + #self.socket.set_shutdown(SSL.SSL_SENT_SHUTDOWN|SSL.SSL_RECEIVED_SHUTDOWN) + self.socket.close() + + def done(self): + "Called when a publishing request is finished" + self.working=0 + self.work() + + def kill_zombies(self): + now = int (time.time()) + for channel in asyncore.socket_map.values(): + if channel.__class__ == self.__class__: + if (now - channel.creation_time) > channel.zombie_timeout: + channel.close() + + +class zhttps_server(https_server): + "https server" + + SERVER_IDENT='ZServerSSL/%s' % (ZSERVER_SSL_VERSION,) + + channel_class = zhttps_channel + shutup = 0 + + def __init__(self, ip, port, ssl_ctx, resolver=None, logger_object=None): + self.shutup = 1 + https_server.__init__(self, ip, port, ssl_ctx, resolver, logger_object) + self.ssl_ctx = ssl_ctx + self.shutup = 0 + self.log_info('(%s) HTTPS server started at %s\n' + '\tHostname: %s\n\tPort: %d' % ( + self.SERVER_IDENT, + time.ctime(time.time()), + self.server_name, + self.server_port + )) + + def log_info(self, message, type='info'): + if self.shutup: return + dispatcher.log_info(self, message, type) + + def readable(self): + return self.accepting and \ + len(asyncore.socket_map) < CONNECTION_LIMIT + + def listen(self, num): + # override asyncore limits for nt's listen queue size + self.accepting = 1 + return self.socket.listen (num) + diff --git a/demo/Zope27/install_dir/lib/python/ZServer/__init__.py.patch b/demo/Zope27/install_dir/lib/python/ZServer/__init__.py.patch new file mode 100644 index 0000000..0ba84b7 --- /dev/null +++ b/demo/Zope27/install_dir/lib/python/ZServer/__init__.py.patch @@ -0,0 +1,10 @@ +--- __init__.py.org Sat Sep 27 20:23:00 2003 ++++ __init__.py Sun Oct 26 18:01:27 2003 +@@ -68,6 +68,7 @@ + import asyncore + from medusa import resolver, logger + from HTTPServer import zhttp_server, zhttp_handler ++from HTTPS_Server import zhttps_server, zhttps0_handler, zhttps_handler + from PCGIServer import PCGIServer + from FCGIServer import FCGIServer + from FTPServer import FTPServer diff --git a/demo/Zope27/install_dir/lib/python/ZServer/component.xml.patch b/demo/Zope27/install_dir/lib/python/ZServer/component.xml.patch new file mode 100644 index 0000000..364d91f --- /dev/null +++ b/demo/Zope27/install_dir/lib/python/ZServer/component.xml.patch @@ -0,0 +1,28 @@ +--- component.xml.org Sat Sep 27 20:21:22 2003 ++++ component.xml Sat Sep 27 21:11:26 2003 +@@ -21,6 +21,25 @@ + </key> + </sectiontype> + ++ <sectiontype name="https-server" ++ datatype=".HTTPS_ServerFactory" ++ implements="ZServer.server"> ++ <key name="address" datatype="inet-address"/> ++ <key name="force-connection-close" datatype="boolean" default="off"/> ++ <key name="webdav-source-clients"> ++ <description> ++ Regular expression used to identify clients who should ++ receive WebDAV source responses to GET requests. ++ </description> ++ </key> ++ <key name="x509-remote-user" datatype="boolean" default="on"> ++ <description> ++ If "on", request client X.509 certificate and set REMOTE_USER to ++ said certificate's Subject Distinguished Name. ++ </description> ++ </key> ++ </sectiontype> ++ + <sectiontype name="webdav-source-server" + datatype=".WebDAVSourceServerFactory" + implements="ZServer.server"> diff --git a/demo/Zope27/install_dir/lib/python/ZServer/datatypes.py.patch b/demo/Zope27/install_dir/lib/python/ZServer/datatypes.py.patch new file mode 100644 index 0000000..b2ad9c8 --- /dev/null +++ b/demo/Zope27/install_dir/lib/python/ZServer/datatypes.py.patch @@ -0,0 +1,59 @@ +--- datatypes.py.org Sat Sep 27 20:21:15 2003 ++++ datatypes.py Sun Oct 26 21:19:58 2003 +@@ -72,7 +72,56 @@ + + def createHandler(self): + from ZServer import HTTPServer ++ try: ++ del self.cgienv['HTTPS'] ++ except KeyError: ++ pass + return HTTPServer.zhttp_handler(self.module, '', self.cgienv) ++ ++ ++class HTTPS_ServerFactory(HTTPServerFactory): ++ def __init__(self, section): ++ HTTPServerFactory.__init__(self, section) ++ self.x509_remote_user = section.x509_remote_user ++ from M2Crypto import Rand, SSL ++ Rand.load_file('%s/randpool.dat' % INSTANCE_HOME, -1) ++ ssl_ctx = SSL.Context('sslv23') ++ ssl_ctx.load_cert_chain('%s/ssl/server.pem' % INSTANCE_HOME) ++ ssl_ctx.load_verify_locations('%s/ssl/ca.pem' % INSTANCE_HOME,'') ++ ssl_ctx.load_client_CA('%s/ssl/ca.pem' % INSTANCE_HOME) ++ ssl_ctx.set_session_id_ctx('Zope 2.7.0b2') ++ ssl_ctx.set_tmp_dh('%s/ssl/dh1024.pem' % INSTANCE_HOME) ++ if self.x509_remote_user: ++ ssl_ctx.set_verify(SSL.verify_peer, 10) ++ else: ++ ssl_ctx.set_verify(SSL.verify_none, 10) ++ self.ssl_ctx = ssl_ctx ++ ++ def create(self): ++ from ZServer import HTTPS_Server ++ from ZServer.AccessLogger import access_logger ++ handler = self.createHandler() ++ handler._force_connection_close = self.force_connection_close ++ if self.webdav_source_clients: ++ handler.set_webdav_source_clients(self.webdav_source_clients) ++ server = HTTPS_Server.zhttps_server(ip=self.host, port=self.port, ++ ssl_ctx=self.ssl_ctx, ++ resolver=self.dnsresolver, ++ logger_object=access_logger) ++ server.install_handler(handler) ++ return server ++ ++ def createHandler(self): ++ from ZServer import HTTPS_Server ++ try: ++ del self.cgienv['HTTP'] ++ except KeyError: ++ pass ++ self.cgienv['HTTPS'] = 'ON' ++ if self.x509_remote_user: ++ return HTTPS_Server.zhttps_handler(self.module, '', self.cgienv) ++ else: ++ return HTTPS_Server.zhttps0_handler(self.module, '', self.cgienv) + + + class WebDAVSourceServerFactory(HTTPServerFactory): diff --git a/demo/Zope27/install_dir/lib/python/ZServer/medusa/https_server.py b/demo/Zope27/install_dir/lib/python/ZServer/medusa/https_server.py new file mode 100644 index 0000000..84a0197 --- /dev/null +++ b/demo/Zope27/install_dir/lib/python/ZServer/medusa/https_server.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python + +"""A https server built on Medusa's http_server. + +Copyright (c) 1999-2003 Ng Pheng Siong. All rights reserved.""" + +import asynchat, asyncore, http_server, socket, sys +from M2Crypto import SSL, version + +VERSION_STRING=version + +class https_channel(http_server.http_channel): + + ac_in_buffer_size = 1 << 16 + + def __init__(self, server, conn, addr): + http_server.http_channel.__init__(self, server, conn, addr) + + def send(self, data): + try: + result = self.socket._write_nbio(data) + if result <= 0: + return 0 + else: + self.server.bytes_out.increment(result) + return result + except SSL.SSLError, why: + self.close() + self.log_info('send: closing channel %s %s' % (repr(self), why)) + return 0 + + def recv(self, buffer_size): + try: + result = self.socket._read_nbio(buffer_size) + if result is None: + return '' + elif result == '': + self.close() + return '' + else: + self.server.bytes_in.increment(len(result)) + return result + except SSL.SSLError, why: + self.close() + self.log_info('recv: closing channel %s %s' % (repr(self), why)) + return '' + + +class https_server(http_server.http_server): + + SERVER_IDENT='M2Crypto HTTPS Server (v%s)' % VERSION_STRING + + channel_class=https_channel + + def __init__(self, ip, port, ssl_ctx, resolver=None, logger_object=None): + http_server.http_server.__init__(self, ip, port, resolver, logger_object) + self.ssl_ctx=ssl_ctx + + def handle_accept(self): + # Cribbed from http_server. + self.total_clients.increment() + try: + conn, addr = self.accept() + except socket.error: + # linux: on rare occasions we get a bogus socket back from + # accept. socketmodule.c:makesockaddr complains that the + # address family is unknown. We don't want the whole server + # to shut down because of this. + sys.stderr.write ('warning: server accept() threw an exception\n') + return + + # Turn the vanilla socket into an SSL connection. + try: + ssl_conn=SSL.Connection(self.ssl_ctx, conn) + ssl_conn._setup_ssl(addr) + ssl_conn.accept_ssl() + self.channel_class(self, ssl_conn, addr) + except SSL.SSLError: + pass + + def writeable(self): + return 0 + diff --git a/demo/Zope27/instance_home/README.txt.patch b/demo/Zope27/instance_home/README.txt.patch new file mode 100644 index 0000000..0d39005 --- /dev/null +++ b/demo/Zope27/instance_home/README.txt.patch @@ -0,0 +1,7 @@ +--- README.txt.org Sat Sep 27 20:56:11 2003 ++++ README.txt Sat Sep 27 20:56:44 2003 +@@ -7,3 +7,4 @@ + log/ Log files + Products/ Installed products specific to the instance + var/ Run-time data files, including the object database ++ ssl/ ZServerSSL data files diff --git a/demo/Zope27/instance_home/etc/zope.conf.patch b/demo/Zope27/instance_home/etc/zope.conf.patch new file mode 100644 index 0000000..cc9b093 --- /dev/null +++ b/demo/Zope27/instance_home/etc/zope.conf.patch @@ -0,0 +1,16 @@ +--- zope.conf.org Sat Sep 27 21:03:22 2003 ++++ zope.conf Sat Sep 27 21:05:08 2003 +@@ -650,6 +650,13 @@ + # force-connection-close on + </http-server> + ++<https-server> ++ # valid keys are "address", "force-connection-close" and "x509-remote-user" ++ address 8443 ++ # force-connection-close on ++ x509-remote-user on ++</https-server> ++ + <ftp-server> + # valid key is "address" + address 8021 diff --git a/demo/Zope27/instance_home/ssl/ca.pem b/demo/Zope27/instance_home/ssl/ca.pem new file mode 100644 index 0000000..b7c84a1 --- /dev/null +++ b/demo/Zope27/instance_home/ssl/ca.pem @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDWTCCAsKgAwIBAgIBADANBgkqhkiG9w0BAQQFADCBgDELMAkGA1UEBhMCU0cx +ETAPBgNVBAoTCE0yQ3J5cHRvMRQwEgYDVQQLEwtNMkNyeXB0byBDQTEkMCIGA1UE +AxMbTTJDcnlwdG8gQ2VydGlmaWNhdGUgTWFzdGVyMSIwIAYJKoZIhvcNAQkBFhNu +Z3BzQG5ldG1lbWV0aWMuY29tMB4XDTAxMTIxNTA1NTU0NloXDTA0MTIxNDA1NTU0 +NlowgYAxCzAJBgNVBAYTAlNHMREwDwYDVQQKEwhNMkNyeXB0bzEUMBIGA1UECxML +TTJDcnlwdG8gQ0ExJDAiBgNVBAMTG00yQ3J5cHRvIENlcnRpZmljYXRlIE1hc3Rl +cjEiMCAGCSqGSIb3DQEJARYTbmdwc0BuZXRtZW1ldGljLmNvbTCBnzANBgkqhkiG +9w0BAQEFAAOBjQAwgYkCgYEAx8soJbS719LHK62VVVIQeC3oW0HvFArwPnA0LuEK +q+LaqMOJg1rS7hvFdX03diV+XJw7cC0iECZYJNG4ii1xbY6KRmufkInaAwm54E3N +e+YYVocaqUkcN6xVf6fwnLfPXbpFS/K2Umg11ObKMmi80JmiIdjcjRRCQZC7g1hf +q+kCAwEAAaOB4DCB3TAdBgNVHQ4EFgQU6/qcBzEtQphfXLhiOHbt2KqBwMIwga0G +A1UdIwSBpTCBooAU6/qcBzEtQphfXLhiOHbt2KqBwMKhgYakgYMwgYAxCzAJBgNV +BAYTAlNHMREwDwYDVQQKEwhNMkNyeXB0bzEUMBIGA1UECxMLTTJDcnlwdG8gQ0Ex +JDAiBgNVBAMTG00yQ3J5cHRvIENlcnRpZmljYXRlIE1hc3RlcjEiMCAGCSqGSIb3 +DQEJARYTbmdwc0BuZXRtZW1ldGljLmNvbYIBADAMBgNVHRMEBTADAQH/MA0GCSqG +SIb3DQEBBAUAA4GBAD+I14GuS5vJmyv1k7mUMbAicsWRHZ+zrGOq9L/L2LsA+lKQ +dAzEZE2+Zv8LBPJVltbJJhcFNJS/ZMAjEm4xlJuCpvXVMxd/M5AM29aqekWlIK7J +vsdDL8IuzpRkMniUiNKPhmB6IPIOslvUKx6QofcE0wDh6pg4VvIbCjkpZ7gf +-----END CERTIFICATE----- diff --git a/demo/Zope27/instance_home/ssl/dh1024.pem b/demo/Zope27/instance_home/ssl/dh1024.pem new file mode 100644 index 0000000..81d43f6 --- /dev/null +++ b/demo/Zope27/instance_home/ssl/dh1024.pem @@ -0,0 +1,5 @@ +-----BEGIN DH PARAMETERS----- +MIGHAoGBAJf2QmHKtQXdKCjhPx1ottPb0PMTBH9A6FbaWMsTuKG/K3g6TG1Z1fkq +/Gz/PWk/eLI9TzFgqVAuPvr3q14a1aZeVUMTgo2oO5/y2UHe6VaJ+trqCTat3xlx +/mNbIK9HA2RgPC3gWfVLZQrY+gz3ASHHR5nXWHEyvpuZm7m3h+irAgEC +-----END DH PARAMETERS----- diff --git a/demo/Zope27/instance_home/ssl/server.pem b/demo/Zope27/instance_home/ssl/server.pem new file mode 100644 index 0000000..1ee9282 --- /dev/null +++ b/demo/Zope27/instance_home/ssl/server.pem @@ -0,0 +1,36 @@ +-----BEGIN CERTIFICATE----- +MIIDYjCCAsugAwIBAgIBBDANBgkqhkiG9w0BAQQFADCBgDELMAkGA1UEBhMCU0cx +ETAPBgNVBAoTCE0yQ3J5cHRvMRQwEgYDVQQLEwtNMkNyeXB0byBDQTEkMCIGA1UE +AxMbTTJDcnlwdG8gQ2VydGlmaWNhdGUgTWFzdGVyMSIwIAYJKoZIhvcNAQkBFhNu +Z3BzQG5ldG1lbWV0aWMuY29tMB4XDTAzMDYyMjEzMzAxNFoXDTA0MDYyMTEzMzAx +NFowXTELMAkGA1UEBhMCU0cxETAPBgNVBAoTCE0yQ3J5cHRvMRIwEAYDVQQDEwls +b2NhbGhvc3QxJzAlBgkqhkiG9w0BCQEWGGFkbWluQHNlcnZlci5leGFtcGxlLmRv +bTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA37aKGZtFicl8xXXTLJ8/JD7c +kd3t/teCX9i61lpaDQCKBoVrrursIvZihemMKI9g/u/+BLqt5g8mBdgUdYz0txc8 +KEbV2hj+wwOX4H3XwD0Y+DysXiNHq7/tFdmzSVHoLxpY4zYzXbxQ/p049wvIyPRp +/y3omcnx/TEUhkn+JmkCAwEAAaOCAQwwggEIMAkGA1UdEwQCMAAwLAYJYIZIAYb4 +QgENBB8WHU9wZW5TU0wgR2VuZXJhdGVkIENlcnRpZmljYXRlMB0GA1UdDgQWBBTB +H/mUYlww24mJlSxEtGdlwojO9zCBrQYDVR0jBIGlMIGigBTr+pwHMS1CmF9cuGI4 +du3YqoHAwqGBhqSBgzCBgDELMAkGA1UEBhMCU0cxETAPBgNVBAoTCE0yQ3J5cHRv +MRQwEgYDVQQLEwtNMkNyeXB0byBDQTEkMCIGA1UEAxMbTTJDcnlwdG8gQ2VydGlm +aWNhdGUgTWFzdGVyMSIwIAYJKoZIhvcNAQkBFhNuZ3BzQG5ldG1lbWV0aWMuY29t +ggEAMA0GCSqGSIb3DQEBBAUAA4GBAAvl6v0s3eFeGP4iAcrfysuK7jzFKhjDYuOy +lVS3u33bZNLnMpM6OSEM9yPh4WpFCVHf+nYwC71pk4ilsLVXjKxymm2lNGcxLVuW +iydFz4Ly9nmN7Ja9ygYT39dGAFP/wN7ELTpsbul8VfmqhNg9y81d8i/A1tK3AGA8 +0QkPQNdP +-----END CERTIFICATE----- +-----BEGIN RSA PRIVATE KEY----- +MIICXAIBAAKBgQDftooZm0WJyXzFddMsnz8kPtyR3e3+14Jf2LrWWloNAIoGhWuu +6uwi9mKF6Ywoj2D+7/4Euq3mDyYF2BR1jPS3FzwoRtXaGP7DA5fgfdfAPRj4PKxe +I0erv+0V2bNJUegvGljjNjNdvFD+nTj3C8jI9Gn/LeiZyfH9MRSGSf4maQIDAQAB +AoGAHpeVtv62uarl9xKvuBBm0AwQmZnhq9HIsFaw5hMg8Vo7hbzFBvx1IirTOkC/ +u+QvfW1QLVFh6m3z4ySzV4fZBtcd6F9SbSrZ0xsxIUB2NOSa1RGgiaP61bJnMMM1 +xM3O9iwM5GZc3Gqy08QOCpDl0772VJ+9Gz3FA88mrc6rHQkCQQDz6RIatFjT28n8 +1vy0nHwwZz2oXTpe/pyZPwoKj8zVsmrKhKwOw7l8ArxjP8zoHOE7AQBCXYDMNoFp +IAF0yuqrAkEA6s0wMEdPpQeb0XHAfccyJQoeULxHdVFoz1wWmGSOm4YmQtR8/QJx +luEgfpeRkzxBKt5Ls3MEkheIOw7xV24zOwJAMz+DaE0AZPNHS3K4ghJnHZxzng6I +lzEUIjbWm0V/ml70hTy/EhMZw+6nOotLOHHo+QbK0SboSwAgzL/Gzo1cJQJANqpS +38qadleRJXAQWrg3qnvyluVe1aeAeVZ9RDmVIgxXeBO0jcs12uTLBe4PzHGo0mwy +v7K1i7XC180gzzQu5QJBAOxITT9RoWSSozPvnirHd37sn+RsrNYkV07NAa80M20Z +DkBPHeMVkNgigrQ6L6vWmbRDGQbGcMplAxnI5ppKCoU= +-----END RSA PRIVATE KEY----- diff --git a/demo/ZopeX3/INSTALL.txt b/demo/ZopeX3/INSTALL.txt new file mode 100644 index 0000000..67adbc8 --- /dev/null +++ b/demo/ZopeX3/INSTALL.txt @@ -0,0 +1,73 @@ +========================================= + ZServerSSL for ZopeX3.0.0 +========================================= + +:Author: Ng Pheng Siong +:Id: $Id: INSTALL.txt 354 2006-02-25 00:13:15Z heikki $ +:Date: $Date$ +:Web-Site: http://sandbox.rulemaker.net/ngps/zope/zssl/ + +.. contents:: + + +Directories +----------------------- + +Directory structure assumes the following: + +- ZopeX3.0.0 is installed in <install-dir>. +- An instance has been created in <instance-home>. + + +<install-dir> +----------------------- + +The following files are to be copied to the corresponding directories +in your <install-dir>: + +- install_dir/lib/python/zope/server/http/https_server.py +- install_dir/lib/python/zope/server/http/https_serverchannel.py +- install_dir/lib/python/zope/server/http/publisherhttps_server.py +- install_dir/lib/python/zope/app/server/https.py + + +<instance-home> +----------------------- + +The following files are to be copied to the corresponding directories +in your <instance-home>: + +- instance_home/ssl/ca.pem +- instance_home/ssl/server.pem +- instance_home/ssl/dh1024.pem + +These are example files. For more information on them, consult the +ZServerSSL HOWTO for Zope 2.6. + +The following patch files are to be applied to the corresponding +directories: + +- install_dir/lib/python/zope/app/server/configure.zcml.patch +- instance_home/etc/zope.conf.patch + + +Launch ZServerSSL +------------------- + +:: + + $ <instance-home>/bin/runzope + + +Testing +--------- + +This section TDB. I have tested ZServerSSL for Zope 3 with 'openssl +s_client' and 'openssl s_client -nbio' successfully. + + +Conclusion +------------ + +Yes, it works! ;-) + diff --git a/demo/ZopeX3/install_dir/lib/python/zope/app/server/configure.zcml.patch b/demo/ZopeX3/install_dir/lib/python/zope/app/server/configure.zcml.patch new file mode 100644 index 0000000..8d8f031 --- /dev/null +++ b/demo/ZopeX3/install_dir/lib/python/zope/app/server/configure.zcml.patch @@ -0,0 +1,28 @@ +--- configure.zcml.org Mon Sep 27 16:10:47 2004 ++++ configure.zcml Mon Sep 27 16:11:42 2004 +@@ -5,6 +5,12 @@ + provides="zope.app.applicationcontrol.interfaces.IServerControl" /> + + <utility ++ name="HTTPS" ++ component=".https.https" ++ provides=".servertype.IServerType" ++ /> ++ ++ <utility + name="HTTP" + component=".http.http" + provides=".servertype.IServerType" +@@ -13,6 +19,12 @@ + <utility + name="PostmortemDebuggingHTTP" + component=".http.pmhttp" ++ provides=".servertype.IServerType" ++ /> ++ ++ <utility ++ name="PostmortemDebuggingHTTPS" ++ component=".https.pmhttps" + provides=".servertype.IServerType" + /> + diff --git a/demo/ZopeX3/install_dir/lib/python/zope/app/server/https.py b/demo/ZopeX3/install_dir/lib/python/zope/app/server/https.py new file mode 100644 index 0000000..2278f3e --- /dev/null +++ b/demo/ZopeX3/install_dir/lib/python/zope/app/server/https.py @@ -0,0 +1,28 @@ +############################################################################## +# +# Copyright (c) 2004, Ng Pheng Siong. +# All Rights Reserved. +# +# XXX license TBD; should be Zope 3's ZPL, I just haven't read thru that. +# +############################################################################## +"""HTTPS server factories + +$Id: https.py 240 2004-10-02 12:40:14Z ngps $ +""" + +from zope.app.publication.httpfactory import HTTPPublicationRequestFactory +from zope.app.server.servertype import ServerType +from zope.server.http.commonaccesslogger import CommonAccessLogger +from zope.server.http.publisherhttps_server import PMDBHTTPS_Server +from zope.server.http.publisherhttps_server import PublisherHTTPS_Server + +https = ServerType(PublisherHTTPS_Server, + HTTPPublicationRequestFactory, + CommonAccessLogger, + 8443, True) + +pmhttps = ServerType(PMDBHTTPS_Server, + HTTPPublicationRequestFactory, + CommonAccessLogger, + 8376, True) diff --git a/demo/ZopeX3/install_dir/lib/python/zope/server/http/https_server.py b/demo/ZopeX3/install_dir/lib/python/zope/server/http/https_server.py new file mode 100644 index 0000000..9e1eb3e --- /dev/null +++ b/demo/ZopeX3/install_dir/lib/python/zope/server/http/https_server.py @@ -0,0 +1,104 @@ +############################################################################## +# +# Copyright (c) 2004, Ng Pheng Siong. +# All Rights Reserved. +# +# XXX license TBD; should be Zope 3's ZPL, I just haven't read thru that. +# +############################################################################## +"""HTTPS Server + +This is a HTTPS version of HTTPServer. + +$Id: https_server.py 240 2004-10-02 12:40:14Z ngps $ +""" + +import asyncore, logging, os.path + +from zope.server.http.httpserver import HTTPServer +from zope.server.http.https_serverchannel import HTTPS_ServerChannel +from M2Crypto import SSL, version + + +# 2004-09-27, ngps: +# 'sslv2' or 'sslv23' interoperates with Firefox and IE. +# 'sslv3' or 'tlsv1' doesn't. +def make_ssl_context(dir, ssl_proto='sslv23'): + sslctx = SSL.Context(ssl_proto) + sslctx.load_cert(os.path.join(dir, 'server.pem')) + sslctx.load_verify_locations(os.path.join(dir, 'ca.pem')) + sslctx.load_client_CA(os.path.join(dir, 'ca.pem')) + sslctx.set_verify(SSL.verify_none, 10) + sslctx.set_session_id_ctx('someblahblahthing') + sslctx.set_tmp_dh(os.path.join(dir, 'dh1024.pem')) + #sslctx.set_info_callback() # debugging only; not thread-safe + return sslctx + + +class HTTPS_Server(HTTPServer): + """This is a generic HTTPS Server.""" + + channel_class = HTTPS_ServerChannel + SERVER_IDENT = 'zope.server.zserverssl_https' + + def __init__(self, ip, port, ssl_ctx=None, task_dispatcher=None, adj=None, start=1, + hit_log=None, verbose=0): + HTTPServer.__init__(self, ip, port, task_dispatcher, adj, start, hit_log, verbose) + if ssl_ctx is None: + self.ssl_ctx = make_ssl_context(os.path.realpath(__file__)) + else: + self.ssl_ctx = ssl_ctx + + def executeRequest(self, task): + """Execute an HTTP request.""" + # This is a default implementation, meant to be overridden. + body = "The HTTPS server is running!\r\n" * 10 + task.response_headers['Content-Type'] = 'text/plain' + task.response_headers['Content-Length'] = str(len(body)) + task.write(body) + + def handle_accept(self): + """See zope.server.interfaces.IDispatcherEventHandler""" + try: + v = self.accept() + if v is None: + return + conn, addr = v + except socket.error: + # Linux: On rare occasions we get a bogus socket back from + # accept. socketmodule.c:makesockaddr complains that the + # address family is unknown. We don't want the whole server + # to shut down because of this. + if self.adj.log_socket_errors: + self.log_info ('warning: server accept() threw an exception', + 'warning') + return + for (level, optname, value) in self.adj.socket_options: + conn.setsockopt(level, optname, value) + # Turn the vanilla socket into an SSL connection. + try: + ssl_conn = SSL.Connection(self.ssl_ctx, conn) + ssl_conn._setup_ssl(addr) + ssl_conn.accept_ssl() + self.channel_class(self, ssl_conn, addr, self.adj) + except SSL.SSLError, why: + self.log_info('accept: cannot make SSL connection %s' % (why,), 'warning') + pass + + + +if __name__ == '__main__': + + from zope.server.taskthreads import ThreadedTaskDispatcher + td = ThreadedTaskDispatcher() + td.setThreadCount(4) + HTTPS_Server('', 8443, ssl_ctx=None, task_dispatcher=td, verbose=1) + + try: + import asyncore + while 1: + asyncore.poll(5) + + except KeyboardInterrupt: + print 'shutting down...' + td.shutdown() diff --git a/demo/ZopeX3/install_dir/lib/python/zope/server/http/https_serverchannel.py b/demo/ZopeX3/install_dir/lib/python/zope/server/http/https_serverchannel.py new file mode 100644 index 0000000..c33a84d --- /dev/null +++ b/demo/ZopeX3/install_dir/lib/python/zope/server/http/https_serverchannel.py @@ -0,0 +1,55 @@ +############################################################################## +# +# Copyright (c) 2004, Ng Pheng Siong. +# All Rights Reserved. +# +# XXX license TBD; should be Zope 3's ZPL, I just haven't read thru that. +# +############################################################################## +"""HTTPS Server Channel + +$Id: https_serverchannel.py 240 2004-10-02 12:40:14Z ngps $ +""" +from zope.server.serverchannelbase import ServerChannelBase +from zope.server.http.httptask import HTTPTask +from zope.server.http.httprequestparser import HTTPRequestParser +from zope.server.http.httpserverchannel import HTTPServerChannel +from M2Crypto import SSL + + +class HTTPS_ServerChannel(HTTPServerChannel): + """HTTPS-specific Server Channel""" + + task_class = HTTPTask + parser_class = HTTPRequestParser + + def send(self, data): + try: + result = self.socket._write_nbio(data) + if result <= 0: + return 0 + else: + #self.server.bytes_out.increment(result) + return result + except SSL.SSLError, why: + self.close() + self.log_info('send: closing channel %s %s' % (repr(self), why), 'warning') + return 0 + + def recv(self, buffer_size): + try: + result = self.socket._read_nbio(buffer_size) + if result is None: + return '' + elif result == '': + self.close() + return '' + else: + #self.server.bytes_in.increment(len(result)) + return result + except SSL.SSLError, why: + self.close() + self.log_info('recv: closing channel %s %s' % (repr(self), why), 'warning') + return '' + + diff --git a/demo/ZopeX3/install_dir/lib/python/zope/server/http/publisherhttps_server.py b/demo/ZopeX3/install_dir/lib/python/zope/server/http/publisherhttps_server.py new file mode 100644 index 0000000..aa8738d --- /dev/null +++ b/demo/ZopeX3/install_dir/lib/python/zope/server/http/publisherhttps_server.py @@ -0,0 +1,83 @@ +############################################################################## +# +# Copyright (c) 2004, Ng Pheng Siong. +# All Rights Reserved. +# +# XXX license TBD; should be Zope 3's ZPL, I just haven't read thru that. +# +############################################################################## +"""HTTPS Server that uses the Zope Publisher for executing a task. + +$Id: publisherhttps_server.py 240 2004-10-02 12:40:14Z ngps $ +""" +import os.path, sys +from zope.server.http.https_server import HTTPS_Server, make_ssl_context +from zope.publisher.publish import publish + + +def get_instance_ssldir(): + # This is real cheesy: It seems Zope3 doesn't have convenient + # programmatic access to INSTANCE_HOME. This code relies on zopectl + # setting the first entry of PYTHONPATH to $INSTANCE_HOME/lib/python. + return os.path.join(os.path.dirname(os.path.dirname(sys.path[0])), 'ssl') + + +class PublisherHTTPS_Server(HTTPS_Server): + """Zope Publisher-specific HTTPS Server""" + + def __init__(self, request_factory, sub_protocol=None, *args, **kw): + + # The common HTTP + self.request_factory = request_factory + + # An HTTP server is not limited to serving up HTML; it can be + # used for other protocols, like XML-RPC, SOAP and so as well + # Here we just allow the logger to output the sub-protocol type. + if sub_protocol: + self.SERVER_IDENT += ' (%s)' %str(sub_protocol) + + kw['ssl_ctx'] = make_ssl_context(get_instance_ssldir()) + HTTPS_Server.__init__(self, *args, **kw) + + def executeRequest(self, task): + """Overrides HTTPServer.executeRequest().""" + env = task.getCGIEnvironment() + env['HTTPS'] = 'ON' + try: + del env['HTTP'] + except KeyError: + pass + instream = task.request_data.getBodyStream() + + request = self.request_factory(instream, task, env) + response = request.response + response.setHeaderOutput(task) + response.setHTTPTransaction(task) + publish(request) + + +class PMDBHTTPS_Server(PublisherHTTPS_Server): + """Enter the post-mortem debugger when there's an error""" + + def executeRequest(self, task): + """Overrides HTTPServer.executeRequest().""" + env = task.getCGIEnvironment() + env['HTTPS'] = 'ON' + try: + del env['HTTP'] + except KeyError: + pass + instream = task.request_data.getBodyStream() + + request = self.request_factory(instream, task, env) + response = request.response + response.setHeaderOutput(task) + try: + publish(request, handle_errors=False) + except: + import sys, pdb + print "%s:" % sys.exc_info()[0] + print sys.exc_info()[1] + pdb.post_mortem(sys.exc_info()[2]) + raise + diff --git a/demo/ZopeX3/instance_home/etc/zope.conf.patch b/demo/ZopeX3/instance_home/etc/zope.conf.patch new file mode 100644 index 0000000..f281246 --- /dev/null +++ b/demo/ZopeX3/instance_home/etc/zope.conf.patch @@ -0,0 +1,26 @@ +--- zope.conf.org Tue Sep 28 09:49:02 2004 ++++ zope.conf Tue Sep 28 09:49:27 2004 +@@ -20,6 +20,11 @@ + address 8080 + </server> + ++<server> ++ type HTTPS ++ address 8443 ++</server> ++ + # For debugging purposes, you can use this publisher instead/as well + # (obviously if it's as well, use a different port number). If there's + # an exception, Zope will drop into pdb at the point of the exception. +@@ -27,6 +32,11 @@ + #<server> + # type PostmortemDebuggingHTTP + # address 8080 ++#</server> ++# ++#<server> ++# type PostmortemDebuggingHTTPS ++# address 8443 + #</server> + + <server> diff --git a/demo/ZopeX3/instance_home/ssl/ca.pem b/demo/ZopeX3/instance_home/ssl/ca.pem new file mode 100644 index 0000000..b7c84a1 --- /dev/null +++ b/demo/ZopeX3/instance_home/ssl/ca.pem @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDWTCCAsKgAwIBAgIBADANBgkqhkiG9w0BAQQFADCBgDELMAkGA1UEBhMCU0cx +ETAPBgNVBAoTCE0yQ3J5cHRvMRQwEgYDVQQLEwtNMkNyeXB0byBDQTEkMCIGA1UE +AxMbTTJDcnlwdG8gQ2VydGlmaWNhdGUgTWFzdGVyMSIwIAYJKoZIhvcNAQkBFhNu +Z3BzQG5ldG1lbWV0aWMuY29tMB4XDTAxMTIxNTA1NTU0NloXDTA0MTIxNDA1NTU0 +NlowgYAxCzAJBgNVBAYTAlNHMREwDwYDVQQKEwhNMkNyeXB0bzEUMBIGA1UECxML +TTJDcnlwdG8gQ0ExJDAiBgNVBAMTG00yQ3J5cHRvIENlcnRpZmljYXRlIE1hc3Rl +cjEiMCAGCSqGSIb3DQEJARYTbmdwc0BuZXRtZW1ldGljLmNvbTCBnzANBgkqhkiG +9w0BAQEFAAOBjQAwgYkCgYEAx8soJbS719LHK62VVVIQeC3oW0HvFArwPnA0LuEK +q+LaqMOJg1rS7hvFdX03diV+XJw7cC0iECZYJNG4ii1xbY6KRmufkInaAwm54E3N +e+YYVocaqUkcN6xVf6fwnLfPXbpFS/K2Umg11ObKMmi80JmiIdjcjRRCQZC7g1hf +q+kCAwEAAaOB4DCB3TAdBgNVHQ4EFgQU6/qcBzEtQphfXLhiOHbt2KqBwMIwga0G +A1UdIwSBpTCBooAU6/qcBzEtQphfXLhiOHbt2KqBwMKhgYakgYMwgYAxCzAJBgNV +BAYTAlNHMREwDwYDVQQKEwhNMkNyeXB0bzEUMBIGA1UECxMLTTJDcnlwdG8gQ0Ex +JDAiBgNVBAMTG00yQ3J5cHRvIENlcnRpZmljYXRlIE1hc3RlcjEiMCAGCSqGSIb3 +DQEJARYTbmdwc0BuZXRtZW1ldGljLmNvbYIBADAMBgNVHRMEBTADAQH/MA0GCSqG +SIb3DQEBBAUAA4GBAD+I14GuS5vJmyv1k7mUMbAicsWRHZ+zrGOq9L/L2LsA+lKQ +dAzEZE2+Zv8LBPJVltbJJhcFNJS/ZMAjEm4xlJuCpvXVMxd/M5AM29aqekWlIK7J +vsdDL8IuzpRkMniUiNKPhmB6IPIOslvUKx6QofcE0wDh6pg4VvIbCjkpZ7gf +-----END CERTIFICATE----- diff --git a/demo/ZopeX3/instance_home/ssl/dh1024.pem b/demo/ZopeX3/instance_home/ssl/dh1024.pem new file mode 100644 index 0000000..81d43f6 --- /dev/null +++ b/demo/ZopeX3/instance_home/ssl/dh1024.pem @@ -0,0 +1,5 @@ +-----BEGIN DH PARAMETERS----- +MIGHAoGBAJf2QmHKtQXdKCjhPx1ottPb0PMTBH9A6FbaWMsTuKG/K3g6TG1Z1fkq +/Gz/PWk/eLI9TzFgqVAuPvr3q14a1aZeVUMTgo2oO5/y2UHe6VaJ+trqCTat3xlx +/mNbIK9HA2RgPC3gWfVLZQrY+gz3ASHHR5nXWHEyvpuZm7m3h+irAgEC +-----END DH PARAMETERS----- diff --git a/demo/ZopeX3/instance_home/ssl/server.pem b/demo/ZopeX3/instance_home/ssl/server.pem new file mode 100644 index 0000000..1ee9282 --- /dev/null +++ b/demo/ZopeX3/instance_home/ssl/server.pem @@ -0,0 +1,36 @@ +-----BEGIN CERTIFICATE----- +MIIDYjCCAsugAwIBAgIBBDANBgkqhkiG9w0BAQQFADCBgDELMAkGA1UEBhMCU0cx +ETAPBgNVBAoTCE0yQ3J5cHRvMRQwEgYDVQQLEwtNMkNyeXB0byBDQTEkMCIGA1UE +AxMbTTJDcnlwdG8gQ2VydGlmaWNhdGUgTWFzdGVyMSIwIAYJKoZIhvcNAQkBFhNu +Z3BzQG5ldG1lbWV0aWMuY29tMB4XDTAzMDYyMjEzMzAxNFoXDTA0MDYyMTEzMzAx +NFowXTELMAkGA1UEBhMCU0cxETAPBgNVBAoTCE0yQ3J5cHRvMRIwEAYDVQQDEwls +b2NhbGhvc3QxJzAlBgkqhkiG9w0BCQEWGGFkbWluQHNlcnZlci5leGFtcGxlLmRv +bTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA37aKGZtFicl8xXXTLJ8/JD7c +kd3t/teCX9i61lpaDQCKBoVrrursIvZihemMKI9g/u/+BLqt5g8mBdgUdYz0txc8 +KEbV2hj+wwOX4H3XwD0Y+DysXiNHq7/tFdmzSVHoLxpY4zYzXbxQ/p049wvIyPRp +/y3omcnx/TEUhkn+JmkCAwEAAaOCAQwwggEIMAkGA1UdEwQCMAAwLAYJYIZIAYb4 +QgENBB8WHU9wZW5TU0wgR2VuZXJhdGVkIENlcnRpZmljYXRlMB0GA1UdDgQWBBTB +H/mUYlww24mJlSxEtGdlwojO9zCBrQYDVR0jBIGlMIGigBTr+pwHMS1CmF9cuGI4 +du3YqoHAwqGBhqSBgzCBgDELMAkGA1UEBhMCU0cxETAPBgNVBAoTCE0yQ3J5cHRv +MRQwEgYDVQQLEwtNMkNyeXB0byBDQTEkMCIGA1UEAxMbTTJDcnlwdG8gQ2VydGlm +aWNhdGUgTWFzdGVyMSIwIAYJKoZIhvcNAQkBFhNuZ3BzQG5ldG1lbWV0aWMuY29t +ggEAMA0GCSqGSIb3DQEBBAUAA4GBAAvl6v0s3eFeGP4iAcrfysuK7jzFKhjDYuOy +lVS3u33bZNLnMpM6OSEM9yPh4WpFCVHf+nYwC71pk4ilsLVXjKxymm2lNGcxLVuW +iydFz4Ly9nmN7Ja9ygYT39dGAFP/wN7ELTpsbul8VfmqhNg9y81d8i/A1tK3AGA8 +0QkPQNdP +-----END CERTIFICATE----- +-----BEGIN RSA PRIVATE KEY----- +MIICXAIBAAKBgQDftooZm0WJyXzFddMsnz8kPtyR3e3+14Jf2LrWWloNAIoGhWuu +6uwi9mKF6Ywoj2D+7/4Euq3mDyYF2BR1jPS3FzwoRtXaGP7DA5fgfdfAPRj4PKxe +I0erv+0V2bNJUegvGljjNjNdvFD+nTj3C8jI9Gn/LeiZyfH9MRSGSf4maQIDAQAB +AoGAHpeVtv62uarl9xKvuBBm0AwQmZnhq9HIsFaw5hMg8Vo7hbzFBvx1IirTOkC/ +u+QvfW1QLVFh6m3z4ySzV4fZBtcd6F9SbSrZ0xsxIUB2NOSa1RGgiaP61bJnMMM1 +xM3O9iwM5GZc3Gqy08QOCpDl0772VJ+9Gz3FA88mrc6rHQkCQQDz6RIatFjT28n8 +1vy0nHwwZz2oXTpe/pyZPwoKj8zVsmrKhKwOw7l8ArxjP8zoHOE7AQBCXYDMNoFp +IAF0yuqrAkEA6s0wMEdPpQeb0XHAfccyJQoeULxHdVFoz1wWmGSOm4YmQtR8/QJx +luEgfpeRkzxBKt5Ls3MEkheIOw7xV24zOwJAMz+DaE0AZPNHS3K4ghJnHZxzng6I +lzEUIjbWm0V/ml70hTy/EhMZw+6nOotLOHHo+QbK0SboSwAgzL/Gzo1cJQJANqpS +38qadleRJXAQWrg3qnvyluVe1aeAeVZ9RDmVIgxXeBO0jcs12uTLBe4PzHGo0mwy +v7K1i7XC180gzzQu5QJBAOxITT9RoWSSozPvnirHd37sn+RsrNYkV07NAa80M20Z +DkBPHeMVkNgigrQ6L6vWmbRDGQbGcMplAxnI5ppKCoU= +-----END RSA PRIVATE KEY----- diff --git a/demo/bio_mem_rw.py b/demo/bio_mem_rw.py new file mode 100644 index 0000000..bcb78b8 --- /dev/null +++ b/demo/bio_mem_rw.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python2.0 + +"""Demonstrates the use of m2.bio_set_mem_eof_return(). +Copyright (c) 1999-2003 Ng Pheng Siong. All rights reserved.""" + +from M2Crypto import m2 +m2.lib_init() + +use_mem = 1 + +if use_mem: + bio = m2.bio_new(m2.bio_s_mem()) +else: + bio = m2.bio_new_file('XXX', 'wb') +ciph = m2.bf_cbc() +filt = m2.bio_new(m2.bio_f_cipher()) +m2.bio_set_cipher(filt, ciph, 'key', 'iv', 1) +m2.bio_push(filt, bio) +m2.bio_write(filt, '12345678901234567890') +m2.bio_flush(filt) +m2.bio_pop(filt) +m2.bio_free(filt) +if use_mem: + m2.bio_set_mem_eof_return(bio, 0) + xxx = m2.bio_read(bio, 100) + print `xxx`, len(xxx) +m2.bio_free(bio) + +if use_mem: + bio = m2.bio_new(m2.bio_s_mem()) + m2.bio_write(bio, xxx) + m2.bio_set_mem_eof_return(bio, 0) +else: + bio = m2.bio_new_file('XXX', 'rb') +ciph = m2.bf_cbc() +filt = m2.bio_new(m2.bio_f_cipher()) +m2.bio_set_cipher(filt, ciph, 'key', 'iv', 0) +m2.bio_push(filt, bio) +yyy = m2.bio_read(filt, 100) +print `yyy` +m2.bio_pop(filt) +m2.bio_free(filt) +m2.bio_free(bio) + diff --git a/demo/dhtest.py b/demo/dhtest.py new file mode 100644 index 0000000..9fd87c6 --- /dev/null +++ b/demo/dhtest.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python + +"""DH demonstration. + +Copyright (c) 1999-2003 Ng Pheng Siong. All rights reserved.""" + +from M2Crypto import DH, Rand + +def test(): + print 'generating dh params:' + a = DH.gen_params(128, 2) + b = DH.set_params(a.p, a.g) + a.gen_key() + b.gen_key() + print 'p = ', `a.p` + print 'g = ', `a.g` + print 'a.pub =', `a.pub` + print 'a.priv =', `a.priv` + print 'b.pub =', `b.pub` + print 'b.priv =', `b.priv` + print 'a.key = ', `a.compute_key(b.pub)` + print 'b.key = ', `b.compute_key(a.pub)` + +if __name__=='__main__': + Rand.load_file('randpool.dat', -1) + test() + Rand.save_file('randpool.dat') diff --git a/demo/dsa1024pvtkey.pem b/demo/dsa1024pvtkey.pem new file mode 100644 index 0000000..8379ed1 --- /dev/null +++ b/demo/dsa1024pvtkey.pem @@ -0,0 +1,12 @@ +-----BEGIN DSA PRIVATE KEY----- +MIIBvQIBAAKBgQD08iE3LXjSB/lA6Gq19XMzfmTvRBgFn+t9qNN6awFhrkowgyrI +HaR6oCCHSvUcjdC0JcJdz8lSqofZkPknX6EEDkmlzZiUhtTZf0XiooeKigAiSlE2 +PpoS4RFcOOqOwVwRJI3mC2lzypI46/OPS0IOZFsxhXQpn1xnkmpEt83ZwwIVAPNw +e6agkM25mv12Il7IuBBNl3cPAoGBAKOES1BV2E3zWj6gXOXFP02dk5A+zd7z4Qj+ +cx5euat07cAHYU0BZGJMTXlHSGf7YQOePuaxs9vTtSeUb1TeMFEr63Jispc9Kzce +rd+E/IjiX7KCMbeGHnhtzC0FU/squZ76vp1TAXSozpfBvn73zAwAFPo/rHO4k6kH +lXAez1QqAoGBAN6iYzbOnMckBtouHGBrdF4ea750DYnH5O2cij+yjgLMttuaxmZe +0iFtJpXp6m4IHKAzIGgKhUGabAz+4O2/ZnmNu0oZzXkpBLL84pksDd0nObtgueL6 +sdTbhGl1kqpWRiK9T16gwqYxdcZiG5M5qbWtJIWWdv3mI9ql0XfUPWfpAhUA8e81 +iGFXunNE3ecKjCOKUL2EnEA= +-----END DSA PRIVATE KEY----- diff --git a/demo/dsa_bench.py b/demo/dsa_bench.py new file mode 100644 index 0000000..cd4b1f9 --- /dev/null +++ b/demo/dsa_bench.py @@ -0,0 +1,176 @@ +#!/usr/bin/env python + +""" + DSA demo and benchmark. + + Usage: python -O dsa_bench.py [option option option ...] + where options may include: + makenewkey showpubkey showdigest showprofile + md5 sha1 sha256 sha512 + <key length> + + NB: + DSA is formally defined with SHA-1 and key length 1024. + The OpenSSL implementation actually supports most any + hashing algorithm and key length, as long as the key + length is longer than the digest length. If not SHA-1 + and 1024, you should be very clear. The use of "DSA" + without any qualifiers implies SHA-1 and 1024. + + Larry Bugbee + November 2006 + + + Some portions are Copyright (c) 1999-2003 Ng Pheng Siong. + All rights reserved. + + Portions created by Open Source Applications Foundation + (OSAF) are Copyright (C) 2004 OSAF. All Rights Reserved. + +""" + +from M2Crypto import DSA, EVP, Rand +from M2Crypto.EVP import MessageDigest +import sys, base64 + +# -------------------------------------------------------------- +# program parameters + +makenewkey = 0 # 1 = make/save new key, 0 = use existing +showpubkey = 0 # 1 = show the public key value +showdigest = 0 # 1 = show the digest value +showprofile = 0 # 1 = use the python profiler + +hashalgs = ['md5', 'ripemd160', 'sha1', + 'sha224', 'sha256', 'sha384', 'sha512'] + +# default hashing algorithm +hashalg = 'sha1' + +# default key length +keylen = 1024 + +# number of speed test loops +N1 = N2 = 100 + +# -------------------------------------------------------------- +# functions + +def test(dsa, dgst): + print ' testing signing and verification...', + try: + r,s = dsa.sign(dgst) + except Exception, e: + print '\n\n *** %s *** \n' % e + sys.exit() + if not dsa.verify(dgst, r, s): + print 'not ok' + else: + print 'ok' + +def test_asn1(dsa, dgst): + # XXX Randomly fails: bug in there somewhere... (0.9.4) + print ' testing asn1 signing and verification...', + blob = dsa.sign_asn1(dgst) + if not dsa.verify_asn1(dgst, blob): + print 'not ok' + else: + print 'ok' + +def speed(): + from time import time + t1 = time() + for i in range(N1): + r,s = dsa.sign(dgst) + print ' %d signings: %8.2fs' % (N1, (time() - t1)) + t1 = time() + for i in range(N2): + dsa.verify(dgst, r, s) + print ' %d verifications: %8.2fs' % (N2, (time() - t1)) + +def test_speed(dsa, dgst): + print ' measuring speed...' + if showprofile: + import profile + profile.run('speed()') + else: + speed() + print + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +def main(keylen, hashalg): + global dsa, dgst # this exists ONLY for speed testing + + Rand.load_file('randpool.dat', -1) + + pvtkeyfilename = 'DSA%dpvtkey.pem' % (keylen) + pubkeyfilename = 'DSA%dpubkey.pem' % (keylen) + + if makenewkey: + print ' making and saving a new key' + dsa = DSA.gen_params(keylen) + dsa.gen_key() + dsa.save_key(pvtkeyfilename, None ) # no pswd callback + dsa.save_pub_key(pubkeyfilename) + else: + print ' loading an existing key' + dsa = DSA.load_key(pvtkeyfilename) + print ' dsa key length:', len(dsa) + + if not dsa.check_key(): + raise 'key is not initialised' + + if showpubkey: + dsa_pub = dsa.pub + pub_pem = base64.encodestring(dsa_pub) + print ' PEM public key is: \n',pub_pem + + # since we are testing signing and verification, let's not + # be fussy about the digest. Just make one. + md = EVP.MessageDigest(hashalg) + md.update('can you spell subliminal channel?') + dgst = md.digest() + print ' hash algorithm: %s' % hashalg + if showdigest: + print ' %s digest: \n%s' % (hashalg, base64.encodestring(dgst)) + + test(dsa, dgst) +# test_asn1(dsa, dgst) + test_speed(dsa, dgst) + Rand.save_file('randpool.dat') + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +def print_usage(): + print """ + Usage: python -O %s [option option option ...] + where options may include: + makenewkey showpubkey showdigest showprofile + md5 sha1 sha256 sha512 + <key length> +""" % sys.argv[0] + sys.exit() + +# -------------------------------------------------------------- +# -------------------------------------------------------------- + +if __name__=='__main__': + for arg in sys.argv[1:]: + if arg in hashalgs: hashalg = arg; continue + if arg == 'makenewkey': makenewkey = 1; continue + if arg == 'showpubkey': showpubkey = 1; continue + if arg == 'showdigest': showdigest = 1; continue + if arg == 'showprofile': showprofile = 1; continue + try: + keylen = int(arg) + except: + print '\n *** argument "%s" not understood ***' % arg + print_usage() + + main(keylen, hashalg) + + +# -------------------------------------------------------------- +# -------------------------------------------------------------- +# -------------------------------------------------------------- diff --git a/demo/dsatest.pem b/demo/dsatest.pem new file mode 100644 index 0000000..8d5d97f --- /dev/null +++ b/demo/dsatest.pem @@ -0,0 +1,8 @@ +-----BEGIN DSA PRIVATE KEY----- +MIH4AgEAAkEA0NGZ0GRXdPLh/0c980Ot8ZbfV/DvJ19ZzsDhKXRxNNw36Ms4lb9Y +ZMnJ1CliIDkpHx8sXEak0vkdeB2efGGBPQIVAJY7PF7CiA+jj+t3EyHf/sgVagPP +AkEApkvDehftx8Kt+3GRsYkEgcKqsU6tue+QQOFOFYsCbMq/3rxIEKk0q1PqHfid ++BsMiEY4FFmF5BqmgGAf6+V9twJATbbgPKi/EboVrtBdkTM52LSCQHPa/CEcj322 +0s5Ix1dwojdQaNpq6HhCm6+g9SXPENy9I/PK85YnawI4A6w1pQIULRB2HSm1X14c ++guvmhIobv6wE50= +-----END DSA PRIVATE KEY----- diff --git a/demo/dsatest.py b/demo/dsatest.py new file mode 100644 index 0000000..854b8b9 --- /dev/null +++ b/demo/dsatest.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python + +"""DSA demonstration. + +Copyright (c) 1999-2003 Ng Pheng Siong. All rights reserved.""" + +from M2Crypto import DSA, EVP, Rand + +md=EVP.MessageDigest('sha1') +md.update('can you spell subliminal channel?') +dgst=md.digest() + +d=DSA.load_key('dsatest.pem') + +def test(): + print 'testing signing...', + r,s=d.sign(dgst) + if not d.verify(dgst, r, s): + print 'not ok' + else: + print 'ok' + +def test_asn1(): + # XXX Randomly fails: bug in there somewhere... (0.9.4) + print 'testing asn1 signing...', + blob=d.sign_asn1(dgst) + if not d.verify_asn1(dgst, blob): + print 'not ok' + else: + print 'ok' + +def speed(): + from time import time + N1 = 5242 + N2 = 2621 + t1 = time() + for i in range(N1): + r,s = d.sign(dgst) + print '%d signings: %8.2fs' % (N1, (time() - t1)) + t1 = time() + for i in range(N2): + d.verify(dgst, r, s) + print '%d verifications: %8.2fs' % (N2, (time() - t1)) + +def test_speed(): + print 'measuring speed...' + import profile + profile.run('speed()') + + +if __name__=='__main__': + Rand.load_file('randpool.dat', -1) + test() + test_asn1() + #test_speed() + Rand.save_file('randpool.dat') + diff --git a/demo/ec/ecdhtest.py b/demo/ec/ecdhtest.py new file mode 100644 index 0000000..6d407b6 --- /dev/null +++ b/demo/ec/ecdhtest.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python + +"""ECDH demonstration. + +Copyright (c) 1999-2003 Ng Pheng Siong. All rights reserved. + +Portions copyright (c) 2005-2006 Vrije Universiteit Amsterdam. +All rights reserved.""" + +from M2Crypto import EC,Rand + +def test(): + print 'generating ec keys:' + a=EC.gen_params(EC.NID_sect233k1) + a.gen_key() + b=EC.gen_params(EC.NID_sect233k1) + b.gen_key() + a_shared_key = a.compute_dh_key(b.pub()) + b_shared_key = b.compute_dh_key(a.pub()) + print 'shared key according to a = ', `a_shared_key` + print 'shared key according to b = ', `b_shared_key` + if a_shared_key == b_shared_key: + print 'ok' + else: + print 'not ok' + + +if __name__=='__main__': + Rand.load_file('randpool.dat', -1) + test() + Rand.save_file('randpool.dat') diff --git a/demo/ec/ecdsa_bench.py b/demo/ec/ecdsa_bench.py new file mode 100644 index 0000000..bafceb1 --- /dev/null +++ b/demo/ec/ecdsa_bench.py @@ -0,0 +1,368 @@ +#!/usr/bin/env python + +""" + ECDSA demo and benchmark. + + Usage: python -O ecdsa_bench.py [option option option ...] + where options may include: + makenewkey showpubkey showdigest showprofile + md5 sha1 sha256 sha512 + secp160r1 secp224r1 secp192k1 sect283r1 + sect283k1 secp256k1 secp384r1 secp521r1 + (other curves and hashes are supported, see below) + + Larry Bugbee, June 2006 + + Portions: + Copyright (c) 1999-2003 Ng Pheng Siong. All rights reserved. + Copyright (c) 2005 Vrije Universiteit Amsterdam. All rights reserved. + +""" + +from M2Crypto import EC, EVP, Rand +from M2Crypto.EVP import MessageDigest +import sys, base64 + +# -------------------------------------------------------------- +# program parameters + +makenewkey = 0 # 1 = make/save new key, 0 = use existing +showpubkey = 0 # 1 = show the public key value +showdigest = 0 # 1 = show the digest value +showprofile = 0 # 1 = use the python profiler + +hashalgs = ['md5', 'ripemd160', 'sha1', + 'sha224', 'sha256', 'sha384', 'sha512'] + +curves = ['secp112r1', + 'secp112r2', + 'secp128r1', + 'secp128r2', + 'secp160k1', + 'secp160r1', + 'secp160r2', + 'secp192k1', + 'secp224k1', + 'secp224r1', + 'secp256k1', + 'secp384r1', + 'secp521r1', + 'sect113r1', + 'sect113r2', + 'sect131r1', + 'sect131r2', + 'sect163k1', + 'sect163r1', + 'sect163r2', + 'sect193r1', + 'sect193r2', + 'sect233k1', + 'sect233r1', + 'sect239k1', + 'sect283k1', + 'sect283r1', + 'sect409k1', + 'sect409r1', + 'sect571k1', + 'sect571r1', + 'X9_62_prime192v1', + 'X9_62_prime192v2', + 'X9_62_prime192v3', + 'X9_62_prime239v1', + 'X9_62_prime239v2', + 'X9_62_prime239v3', + 'X9_62_prime256v1', + 'X9_62_c2pnb163v1', + 'X9_62_c2pnb163v2', + 'X9_62_c2pnb163v3', + 'X9_62_c2pnb176v1', + 'X9_62_c2tnb191v1', + 'X9_62_c2tnb191v2', + 'X9_62_c2tnb191v3', + 'X9_62_c2pnb208w1', + 'X9_62_c2tnb239v1', + 'X9_62_c2tnb239v2', + 'X9_62_c2tnb239v3', + 'X9_62_c2pnb272w1', + 'X9_62_c2pnb304w1', + 'X9_62_c2tnb359v1', + 'X9_62_c2pnb368w1', + 'X9_62_c2tnb431r1', + 'wap_wsg_idm_ecid_wtls1', + 'wap_wsg_idm_ecid_wtls3', + 'wap_wsg_idm_ecid_wtls4', + 'wap_wsg_idm_ecid_wtls5', + 'wap_wsg_idm_ecid_wtls6', + 'wap_wsg_idm_ecid_wtls7', + 'wap_wsg_idm_ecid_wtls8', + 'wap_wsg_idm_ecid_wtls9', + 'wap_wsg_idm_ecid_wtls10', + 'wap_wsg_idm_ecid_wtls11', + 'wap_wsg_idm_ecid_wtls12', + ] + +# The following two curves, according to OpenSSL, have a +# "Questionable extension field!" and are not supported by +# the OpenSSL inverse function. ECError: no inverse. +# As such they cannot be used for signing. They might, +# however, be usable for encryption but that has not +# been tested. Until thir usefulness can be established, +# they are not supported at this time. +# +# Oakley-EC2N-3: +# IPSec/IKE/Oakley curve #3 over a 155 bit binary field. +# Oakley-EC2N-4: +# IPSec/IKE/Oakley curve #4 over a 185 bit binary field. +# +# aka 'ipsec3' and 'ipsec4' + +# curves2 is a shorthand convenience so as to not require the +# entering the "X9_62_" prefix +curves2 = ['prime192v1', + 'prime192v2', + 'prime192v3', + 'prime239v1', + 'prime239v2', + 'prime239v3', + 'prime256v1', + 'c2pnb163v1', + 'c2pnb163v2', + 'c2pnb163v3', + 'c2pnb176v1', + 'c2tnb191v1', + 'c2tnb191v2', + 'c2tnb191v3', + 'c2pnb208w1', + 'c2tnb239v1', + 'c2tnb239v2', + 'c2tnb239v3', + 'c2pnb272w1', + 'c2pnb304w1', + 'c2tnb359v1', + 'c2pnb368w1', + 'c2tnb431r1', + ] + +# default hashing algorithm +hashalg = 'sha1' + +# default elliptical curve +curve = 'secp160r1' + +# for a complete list of supported algorithms and curves, see +# the bottom of this file + +# number of speed test loops +N1 = N2 = 100 + +# -------------------------------------------------------------- +# functions + +def test(ec, dgst): + print ' testing signing and verification...', + try: +# ec = EC.gen_params(EC.NID_secp160r1) +# ec.gen_key() + r,s = ec.sign_dsa(dgst) + except Exception, e: + print '\n\n *** %s *** \n' % e + sys.exit() + if not ec.verify_dsa(dgst, r, s): + print 'not ok' + else: + print 'ok' + +def test_asn1(ec, dgst): + print ' testing asn1 signing and verification...', + blob = ec.sign_dsa_asn1(dgst) + if not ec.verify_dsa_asn1(dgst, blob): + print 'not ok' + else: + print 'ok' + +def speed(): + from time import time + t1 = time() + for i in range(N1): + r,s = ec.sign_dsa(dgst) + print ' %d signings: %8.2fs' % (N1, (time() - t1)) + t1 = time() + for i in range(N2): + ec.verify_dsa(dgst, r, s) + print ' %d verifications: %8.2fs' % (N2, (time() - t1)) + +def test_speed(ec, dgst): + print ' measuring speed...' + if showprofile: + import profile + profile.run('speed()') + else: + speed() + print + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +def main(curve, hashalg): + global ec, dgst # this exists ONLY for speed testing + + Rand.load_file('randpool.dat', -1) + + if curve in curves2: + curve = 'X9_62_' + curve + ec_curve = eval('EC.NID_%s' % curve) + + pvtkeyfilename = '%spvtkey.pem' % (curve) + pubkeyfilename = '%spubkey.pem' % (curve) + + if makenewkey: + print ' making and saving a new key' + ec = EC.gen_params(ec_curve) + ec.gen_key() + ec.save_key(pvtkeyfilename, None ) + ec.save_pub_key(pubkeyfilename) + else: + print ' loading an existing key' + ec=EC.load_key(pvtkeyfilename) + print ' ecdsa key length:', len(ec) + print ' curve: %s' % curve + + if not ec.check_key(): + raise 'key is not initialised' + + if showpubkey: + ec_pub = ec.pub() + pub_der = ec_pub.get_der() + pub_pem = base64.encodestring(pub_der) + print ' PEM public key is: \n',pub_pem + + # since we are testing signing and verification, let's not + # be fussy about the digest. Just make one. + md = EVP.MessageDigest(hashalg) + md.update('can you spell subliminal channel?') + dgst = md.digest() + print ' hash algorithm: %s' % hashalg + if showdigest: + print ' %s digest: \n%s' % (base64.encodestring(dgst)) + + test(ec, dgst) +# test_asn1(ec, dgst) + test_speed(ec, dgst) + Rand.save_file('randpool.dat') + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +def print_usage(): + print """ + Usage: python -O %s [option option option ...] + where options may include: + makenewkey showpubkey showdigest showprofile + md5 sha1 sha256 sha512 + secp160r1 secp224r1 secp192k1 sect283r1 + sect283k1 secp256k1 secp384r1 secp521r1 + (other curves and hashes are supported, check pgm src) +""" % sys.argv[0] + sys.exit() + +# -------------------------------------------------------------- +# -------------------------------------------------------------- + +if __name__=='__main__': + for arg in sys.argv[1:]: + if arg in hashalgs: hashalg = arg; continue + if arg in curves + curves2: curve = arg; continue + if arg == 'makenewkey': makenewkey = 1; continue + if arg == 'showpubkey': showpubkey = 1; continue + if arg == 'showdigest': showdigest = 1; continue + if arg == 'showprofile': showprofile = 1; continue + + print '\n *** argument "%s" not understood ***' % arg + print_usage() + + main(curve, hashalg) + + +# -------------------------------------------------------------- +# -------------------------------------------------------------- +# -------------------------------------------------------------- + + +""" + Elliptical curves supported by OpenSSL + ====================================== + +$ openssl ecparam -list_curves + secp112r1 : SECG/WTLS curve over a 112 bit prime field + secp112r2 : SECG curve over a 112 bit prime field + secp128r1 : SECG curve over a 128 bit prime field + secp128r2 : SECG curve over a 128 bit prime field + secp160k1 : SECG curve over a 160 bit prime field + secp160r1 : SECG curve over a 160 bit prime field + secp160r2 : SECG/WTLS curve over a 160 bit prime field + secp192k1 : SECG curve over a 192 bit prime field + secp224k1 : SECG curve over a 224 bit prime field + secp224r1 : NIST/SECG curve over a 224 bit prime field + secp256k1 : SECG curve over a 256 bit prime field + secp384r1 : NIST/SECG curve over a 384 bit prime field + secp521r1 : NIST/SECG curve over a 521 bit prime field + prime192v1: NIST/X9.62/SECG curve over a 192 bit prime field + prime192v2: X9.62 curve over a 192 bit prime field + prime192v3: X9.62 curve over a 192 bit prime field + prime239v1: X9.62 curve over a 239 bit prime field + prime239v2: X9.62 curve over a 239 bit prime field + prime239v3: X9.62 curve over a 239 bit prime field + prime256v1: X9.62/SECG curve over a 256 bit prime field + sect113r1 : SECG curve over a 113 bit binary field + sect113r2 : SECG curve over a 113 bit binary field + sect131r1 : SECG/WTLS curve over a 131 bit binary field + sect131r2 : SECG curve over a 131 bit binary field + sect163k1 : NIST/SECG/WTLS curve over a 163 bit binary field + sect163r1 : SECG curve over a 163 bit binary field + sect163r2 : NIST/SECG curve over a 163 bit binary field + sect193r1 : SECG curve over a 193 bit binary field + sect193r2 : SECG curve over a 193 bit binary field + sect233k1 : NIST/SECG/WTLS curve over a 233 bit binary field + sect233r1 : NIST/SECG/WTLS curve over a 233 bit binary field + sect239k1 : SECG curve over a 239 bit binary field + sect283k1 : NIST/SECG curve over a 283 bit binary field + sect283r1 : NIST/SECG curve over a 283 bit binary field + sect409k1 : NIST/SECG curve over a 409 bit binary field + sect409r1 : NIST/SECG curve over a 409 bit binary field + sect571k1 : NIST/SECG curve over a 571 bit binary field + sect571r1 : NIST/SECG curve over a 571 bit binary field + c2pnb163v1: X9.62 curve over a 163 bit binary field + c2pnb163v2: X9.62 curve over a 163 bit binary field + c2pnb163v3: X9.62 curve over a 163 bit binary field + c2pnb176v1: X9.62 curve over a 176 bit binary field + c2tnb191v1: X9.62 curve over a 191 bit binary field + c2tnb191v2: X9.62 curve over a 191 bit binary field + c2tnb191v3: X9.62 curve over a 191 bit binary field + c2pnb208w1: X9.62 curve over a 208 bit binary field + c2tnb239v1: X9.62 curve over a 239 bit binary field + c2tnb239v2: X9.62 curve over a 239 bit binary field + c2tnb239v3: X9.62 curve over a 239 bit binary field + c2pnb272w1: X9.62 curve over a 272 bit binary field + c2pnb304w1: X9.62 curve over a 304 bit binary field + c2tnb359v1: X9.62 curve over a 359 bit binary field + c2pnb368w1: X9.62 curve over a 368 bit binary field + c2tnb431r1: X9.62 curve over a 431 bit binary field + wap-wsg-idm-ecid-wtls1: WTLS curve over a 113 bit binary field + wap-wsg-idm-ecid-wtls3: NIST/SECG/WTLS curve over a 163 bit binary field + wap-wsg-idm-ecid-wtls4: SECG curve over a 113 bit binary field + wap-wsg-idm-ecid-wtls5: X9.62 curve over a 163 bit binary field + wap-wsg-idm-ecid-wtls6: SECG/WTLS curve over a 112 bit prime field + wap-wsg-idm-ecid-wtls7: SECG/WTLS curve over a 160 bit prime field + wap-wsg-idm-ecid-wtls8: WTLS curve over a 112 bit prime field + wap-wsg-idm-ecid-wtls9: WTLS curve over a 160 bit prime field + wap-wsg-idm-ecid-wtls10: NIST/SECG/WTLS curve over a 233 bit binary field + wap-wsg-idm-ecid-wtls11: NIST/SECG/WTLS curve over a 233 bit binary field + wap-wsg-idm-ecid-wtls12: WTLS curvs over a 224 bit prime field + Oakley-EC2N-3: + IPSec/IKE/Oakley curve #3 over a 155 bit binary field. + Not suitable for ECDSA. + Questionable extension field! + Oakley-EC2N-4: + IPSec/IKE/Oakley curve #4 over a 185 bit binary field. + Not suitable for ECDSA. + Questionable extension field! + +""" diff --git a/demo/ec/ecdsatest.pem b/demo/ec/ecdsatest.pem new file mode 100644 index 0000000..cc37a65 --- /dev/null +++ b/demo/ec/ecdsatest.pem @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MG0CAQEEHVkQ54w5gN39TScPaC+TTKmJunupCfuNqEcWOZeXoAcGBSuBBAAaoUAD +PgAEAIcWBNwIi1fP2Sd33wpayKBuw2oqBVvgvfYiipMcASzxCf6IFUC03IOob/Lu +Y3mPHRZKwzSKBlD1ZkXh +-----END EC PRIVATE KEY----- diff --git a/demo/ec/ecdsatest.py b/demo/ec/ecdsatest.py new file mode 100644 index 0000000..61ad625 --- /dev/null +++ b/demo/ec/ecdsatest.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python + +"""ECDSA demonstration. + +Copyright (c) 1999-2003 Ng Pheng Siong. All rights reserved. +Portions copyright (c) 2005-2006 Vrije Universiteit Amsterdam. All rights reserved. +""" + +from M2Crypto import EC, EVP, Rand +import base64 + +md=EVP.MessageDigest('sha1') +md.update('can you spell subliminal channel?') +dgst=md.digest() + +ec=EC.load_key('ecdsatest.pem') +#ec=EC.gen_params(EC.NID_sect233k1) +#ec.gen_key() +ec_pub = ec.pub() +pub_der = ec_pub.get_der() +pub_pem = base64.encodestring(pub_der) +print 'PEM public key is',pub_pem +ec.save_key( 'ecdsatest.pem', None ) + + +def test(): + print 'testing signing...', + r,s=ec.sign_dsa(dgst) + if not ec.verify_dsa(dgst, r, s): + print 'not ok' + else: + print 'ok' + +def test_asn1(): + # XXX Randomly fails: bug in there somewhere... (0.9.4) + print 'testing asn1 signing...', + blob=ec.sign_dsa_asn1(dgst) + if not ec.verify_dsa_asn1(dgst, blob): + print 'not ok' + else: + print 'ok' + +def speed(): + from time import time + N1 = 5242 + N2 = 2621 + t1 = time() + for i in range(N1): + r,s = ec.sign(dgst) + print '%d signings: %8.2fs' % (N1, (time() - t1)) + t1 = time() + for i in range(N2): + ec.verify(dgst, r, s) + print '%d verifications: %8.2fs' % (N2, (time() - t1)) + +def test_speed(): + print 'measuring speed...' + import profile + profile.run('speed()') + + +if __name__=='__main__': + Rand.load_file('randpool.dat', -1) + test() + test_asn1() + #test_speed() + Rand.save_file('randpool.dat') + diff --git a/demo/ec/secp160r1pvtkey.pem b/demo/ec/secp160r1pvtkey.pem new file mode 100644 index 0000000..6eac827 --- /dev/null +++ b/demo/ec/secp160r1pvtkey.pem @@ -0,0 +1,4 @@ +-----BEGIN EC PRIVATE KEY----- +MFACAQEEFN1C7AYNKDl9dBLfm0QW1nB7WGs7oAcGBSuBBAAIoSwDKgEErnCVbfXH +11Ax4AZq4eh8c3gWIBeaRu6tGRpITFCjtHot78c/oVqBrg== +-----END EC PRIVATE KEY----- diff --git a/demo/https.howto/ca.pem b/demo/https.howto/ca.pem new file mode 100644 index 0000000..d8ba0d3 --- /dev/null +++ b/demo/https.howto/ca.pem @@ -0,0 +1,59 @@ +Certificate: + Data: + Version: 3 (0x2) + Serial Number: 0 (0x0) + Signature Algorithm: sha1WithRSAEncryption + Issuer: C=US, ST=CA, O=M2Crypto CA, CN=localhost + Validity + Not Before: Apr 22 04:35:56 2006 GMT + Not After : Apr 21 04:35:56 2009 GMT + Subject: C=US, ST=CA, O=M2Crypto CA, CN=localhost + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + RSA Public Key: (1024 bit) + Modulus (1024 bit): + 00:d8:80:02:8f:b5:7d:9f:9b:79:76:67:a5:66:64: + c0:30:0c:71:65:f1:c6:78:01:a0:29:d4:3a:2c:e5: + ee:58:4d:db:53:c5:74:6e:4e:f7:b6:a5:8e:ef:ab: + e8:7c:f5:5d:2d:18:ba:95:b8:15:43:6e:5a:78:c2: + 91:05:08:b2:7e:cf:c4:d3:bb:ac:c7:43:27:fb:8f: + 43:0d:7b:d0:d1:32:51:86:11:6e:3e:aa:68:19:88: + b9:cf:d5:72:f0:a4:73:d1:69:c4:65:14:0e:12:64: + 7e:1f:df:18:09:0b:6a:4b:cd:bf:ae:59:82:15:1c: + 90:0f:c3:e5:cb:b3:ed:86:4d + Exponent: 65537 (0x10001) + X509v3 extensions: + X509v3 Basic Constraints: + CA:TRUE + Netscape Comment: + OpenSSL Generated Certificate + X509v3 Subject Key Identifier: + 5C:F9:C5:9B:B0:02:37:3C:66:73:4D:0E:CB:5A:3D:BB:3A:46:22:DD + X509v3 Authority Key Identifier: + keyid:5C:F9:C5:9B:B0:02:37:3C:66:73:4D:0E:CB:5A:3D:BB:3A:46:22:DD + + Signature Algorithm: sha1WithRSAEncryption + 6b:9e:71:4a:ad:d2:1c:b7:58:1a:6e:8b:89:92:8d:4e:62:61: + 06:2e:e8:11:f8:9c:a0:e2:11:7c:b6:e2:be:ef:b9:b1:35:20: + d1:81:62:c5:ca:3c:4f:c9:88:72:f7:50:d8:e8:e0:06:43:ee: + c5:5c:38:9b:e7:24:46:a6:ee:8d:b0:70:4e:75:96:00:db:d6: + 59:f9:58:74:67:9f:ca:9c:12:fc:77:a7:0e:5a:38:22:5b:de: + c9:33:35:bd:d0:4c:9f:6a:0f:71:7b:db:cb:fd:da:bc:39:4f: + 23:1e:74:5b:ff:8d:73:72:16:a9:9f:57:54:96:3e:2c:f0:65: + af:df +-----BEGIN CERTIFICATE----- +MIICfDCCAeWgAwIBAgIBADANBgkqhkiG9w0BAQUFADBEMQswCQYDVQQGEwJVUzEL +MAkGA1UECBMCQ0ExFDASBgNVBAoTC00yQ3J5cHRvIENBMRIwEAYDVQQDEwlsb2Nh +bGhvc3QwHhcNMDYwNDIyMDQzNTU2WhcNMDkwNDIxMDQzNTU2WjBEMQswCQYDVQQG +EwJVUzELMAkGA1UECBMCQ0ExFDASBgNVBAoTC00yQ3J5cHRvIENBMRIwEAYDVQQD +Ewlsb2NhbGhvc3QwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBANiAAo+1fZ+b +eXZnpWZkwDAMcWXxxngBoCnUOizl7lhN21PFdG5O97alju+r6Hz1XS0YupW4FUNu +WnjCkQUIsn7PxNO7rMdDJ/uPQw170NEyUYYRbj6qaBmIuc/VcvCkc9FpxGUUDhJk +fh/fGAkLakvNv65ZghUckA/D5cuz7YZNAgMBAAGjfjB8MAwGA1UdEwQFMAMBAf8w +LAYJYIZIAYb4QgENBB8WHU9wZW5TU0wgR2VuZXJhdGVkIENlcnRpZmljYXRlMB0G +A1UdDgQWBBRc+cWbsAI3PGZzTQ7LWj27OkYi3TAfBgNVHSMEGDAWgBRc+cWbsAI3 +PGZzTQ7LWj27OkYi3TANBgkqhkiG9w0BAQUFAAOBgQBrnnFKrdIct1gabouJko1O +YmEGLugR+Jyg4hF8tuK+77mxNSDRgWLFyjxPyYhy91DY6OAGQ+7FXDib5yRGpu6N +sHBOdZYA29ZZ+Vh0Z5/KnBL8d6cOWjgiW97JMzW90Eyfag9xe9vL/dq8OU8jHnRb +/41zchapn1dUlj4s8GWv3w== +-----END CERTIFICATE----- diff --git a/demo/https.howto/dh1024.pem b/demo/https.howto/dh1024.pem new file mode 100644 index 0000000..81d43f6 --- /dev/null +++ b/demo/https.howto/dh1024.pem @@ -0,0 +1,5 @@ +-----BEGIN DH PARAMETERS----- +MIGHAoGBAJf2QmHKtQXdKCjhPx1ottPb0PMTBH9A6FbaWMsTuKG/K3g6TG1Z1fkq +/Gz/PWk/eLI9TzFgqVAuPvr3q14a1aZeVUMTgo2oO5/y2UHe6VaJ+trqCTat3xlx +/mNbIK9HA2RgPC3gWfVLZQrY+gz3ASHHR5nXWHEyvpuZm7m3h+irAgEC +-----END DH PARAMETERS----- diff --git a/demo/https.howto/get_https.py b/demo/https.howto/get_https.py new file mode 100755 index 0000000..9728fab --- /dev/null +++ b/demo/https.howto/get_https.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python +"""Demonstrations of M2Crypto.httpslib. + +Copyright (c) 1999-2002 Ng Pheng Siong. All rights reserved. + +Portions created by Open Source Applications Foundation (OSAF) are +Copyright (C) 2006 OSAF. All Rights Reserved. +""" + +from M2Crypto import Rand, SSL, httpslib + +def get_https(): + ctx = SSL.Context() + if ctx.load_verify_locations('ca.pem') != 1: + raise Exception('CA certificates not loaded') + ctx.set_verify(SSL.verify_peer | SSL.verify_fail_if_no_peer_cert, 9) + h = httpslib.HTTPSConnection('localhost', 9443, ssl_context=ctx) + h.set_debuglevel(1) + h.putrequest('GET', '/') + h.endheaders() + resp = h.getresponse() + while 1: + data = resp.read() + if not data: + break + print data + h.close() + +Rand.load_file('../randpool.dat', -1) +get_https() +Rand.save_file('../randpool.dat') + diff --git a/demo/https.howto/https_cli.py b/demo/https.howto/https_cli.py new file mode 100644 index 0000000..bb34625 --- /dev/null +++ b/demo/https.howto/https_cli.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python + +"""Demonstrations of M2Crypto.httpslib. + +Copyright (c) 1999-2002 Ng Pheng Siong. All rights reserved. + +Portions created by Open Source Applications Foundation (OSAF) are +Copyright (C) 2006 OSAF. All Rights Reserved. +""" + +import sys +from M2Crypto import Rand, SSL, httpslib, threading + + +def test_httpslib(): + ctx = SSL.Context() + if ctx.load_verify_locations('ca.pem') != 1: + raise Exception('CA certificates not loaded') + ctx.set_verify(SSL.verify_peer | SSL.verify_fail_if_no_peer_cert, 9) + ctx.set_info_callback() + h = httpslib.HTTPSConnection('localhost', 9443, ssl_context=ctx) + h.set_debuglevel(1) + h.putrequest('GET', '/') + h.putheader('Accept', 'text/html') + h.putheader('Accept', 'text/plain') + h.putheader('Connection', 'close') + h.endheaders() + resp = h.getresponse() + f = resp.fp + c = 0 + while 1: + # Either of following two works. + #data = f.readline() + data = resp.read() + if not data: break + c = c + len(data) + sys.stdout.write(data) + sys.stdout.flush() + f.close() + h.close() + + +if __name__=='__main__': + Rand.load_file('../randpool.dat', -1) + #threading.init() + test_httpslib() + #threading.cleanup() + Rand.save_file('../randpool.dat') + diff --git a/demo/https.howto/orig_https_srv.py b/demo/https.howto/orig_https_srv.py new file mode 100644 index 0000000..83be0fb --- /dev/null +++ b/demo/https.howto/orig_https_srv.py @@ -0,0 +1,153 @@ +"""This server extends BaseHTTPServer and SimpleHTTPServer thusly: +1. One thread per connection. +2. Generates directory listings. + +In addition, it has the following properties: +1. Works over HTTPS only. +2. Displays SSL handshaking and SSL session info. +3. Performs SSL renegotiation when a magic url is requested. + +TODO: +1. Cache stat() of directory entries. +2. Fancy directory indexing. +3. Interface ZPublisher. + +Copyright (c) 1999-2000 Ng Pheng Siong. All rights reserved. + +Portions created by Open Source Applications Foundation (OSAF) are +Copyright (C) 2006 OSAF. All Rights Reserved. +""" + +import os, sys +from SimpleHTTPServer import SimpleHTTPRequestHandler + +from M2Crypto import Rand, SSL, threading +from M2Crypto.SSL.SSLServer import ThreadingSSLServer + +try: + from cStringIO import StringIO +except ImportError: + from StringIO import StringIO + + +def mkdirlist(path, url): + dirlist = os.listdir(path) + dirlist.sort() + f = StringIO() + f.write('<title>Index listing for %s</title>\r\n' % (url,)) + f.write('<h1>Index listing for %s</h1>\r\n' % (url,)) + f.write('<pre>\r\n') + for d in dirlist: + if os.path.isdir(os.path.join(path, d)): + d2 = d + '/' + else: + d2 = d + if url == '/': + f.write('<a href="/%s">%s</a><br>\r\n' % (d, d2)) + else: + f.write('<a href="%s/%s">%s</a><br>\r\n' % (url, d, d2)) + f.write('</pre>\r\n\r\n') + f.reset() + return f + + +class HTTP_Handler(SimpleHTTPRequestHandler): + + server_version = "https_srv/0.1" + reneg = 0 + + # Cribbed from SimpleHTTPRequestHander to add the ".der" entry, + # which facilitates installing your own certificates into browsers. + extensions_map = { + '': 'text/plain', # Default, *must* be present + '.html': 'text/html', + '.htm': 'text/html', + '.gif': 'image/gif', + '.jpg': 'image/jpeg', + '.jpeg': 'image/jpeg', + '.der': 'application/x-x509-ca-cert' + } + + def send_head(self): + if self.path[1:8] == '_reneg_': + self.reneg = 1 + self.path = self.path[8:] + path = self.translate_path(self.path) + if os.path.isdir(path): + f = mkdirlist(path, self.path) + filetype = 'text/html' + else: + try: + f = open(path, 'rb') + filetype = self.guess_type(path) + except IOError: + self.send_error(404, "File not found") + return None + self.send_response(200) + self.send_header("Content-type", filetype) + self.end_headers() + return f + + def do_GET(self): + #sess = self.request.get_session() + #self.log_message('\n%s', sess.as_text()) + f = self.send_head() + if self.reneg: + self.reneg = 0 + self.request.renegotiate() + sess = self.request.get_session() + self.log_message('\n%s', sess.as_text()) + if f: + self.copyfile(f, self.wfile) + f.close() + + def do_HEAD(self): + #sess = self.request.get_session() + #self.log_message('\n%s', sess.as_text()) + f = self.send_head() + if f: + f.close() + + +class HTTPS_Server(ThreadingSSLServer): + def __init__(self, server_addr, handler, ssl_ctx): + ThreadingSSLServer.__init__(self, server_addr, handler, ssl_ctx) + self.server_name = server_addr[0] + self.server_port = server_addr[1] + + def finish(self): + self.request.set_shutdown(SSL.SSL_RECEIVED_SHUTDOWN | SSL.SSL_SENT_SHUTDOWN) + self.request.close() + + +def init_context(protocol, certfile, cafile, verify, verify_depth=10): + ctx=SSL.Context(protocol) + ctx.load_cert(certfile) + ctx.load_client_ca(cafile) + if ctx.load_verify_locations(cafile) != 1: + raise Exception('CA certificates not loaded') + ctx.set_verify(verify, verify_depth) + ctx.set_allow_unknown_ca(1) + ctx.set_session_id_ctx('https_srv') + ctx.set_info_callback() + return ctx + + +if __name__ == '__main__': + if len(sys.argv) < 2: + wdir = '.' + else: + wdir = sys.argv[1] + Rand.load_file('../randpool.dat', -1) + threading.init() + ctx = init_context('sslv23', 'server.pem', 'ca.pem', \ + SSL.verify_none) + #SSL.verify_peer | SSL.verify_fail_if_no_peer_cert) + ctx.set_tmp_dh('dh1024.pem') + os.chdir(wdir) + httpsd = HTTPS_Server(('', 9443), HTTP_Handler, ctx) + httpsd.serve_forever() + threading.cleanup() + Rand.save_file('../randpool.dat') + + diff --git a/demo/https.howto/server.pem b/demo/https.howto/server.pem new file mode 100644 index 0000000..bcd826c --- /dev/null +++ b/demo/https.howto/server.pem @@ -0,0 +1,74 @@ +Certificate: + Data: + Version: 3 (0x2) + Serial Number: 1 (0x1) + Signature Algorithm: sha1WithRSAEncryption + Issuer: C=US, ST=CA, O=M2Crypto CA, CN=localhost + Validity + Not Before: Apr 22 04:36:56 2006 GMT + Not After : Apr 22 04:36:56 2007 GMT + Subject: C=US, ST=CA, O=M2Crypto Server, CN=localhost + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + RSA Public Key: (1024 bit) + Modulus (1024 bit): + 00:a5:cc:39:ad:ba:81:3d:bd:05:f4:61:50:9f:9c: + f6:ad:ec:29:d9:78:1e:24:61:f7:1b:36:bf:69:d8: + b3:45:ae:6f:3a:4c:4f:d6:13:6d:60:8d:f2:bb:2a: + c4:1b:79:fd:e2:f8:d6:3c:56:53:3b:27:f7:3f:70: + a4:64:99:63:46:2e:3f:ef:52:da:a9:04:5b:6e:d4: + 40:57:c5:59:61:d3:3f:7d:b8:03:c1:9b:65:46:2a: + c5:9d:70:b7:ca:79:6e:dd:e4:3f:c2:f4:2f:2e:81: + 32:c8:e9:a6:b6:a8:c8:1f:48:be:7a:66:56:98:fc: + 3c:25:fc:d9:3d:73:07:30:71 + Exponent: 65537 (0x10001) + X509v3 extensions: + X509v3 Basic Constraints: + CA:FALSE + Netscape Comment: + OpenSSL Generated Certificate + X509v3 Subject Key Identifier: + F0:48:3A:32:88:8C:80:D7:22:AB:56:F8:86:B3:04:47:10:76:37:BE + X509v3 Authority Key Identifier: + keyid:5C:F9:C5:9B:B0:02:37:3C:66:73:4D:0E:CB:5A:3D:BB:3A:46:22:DD + + Signature Algorithm: sha1WithRSAEncryption + 39:47:95:5c:ea:7e:db:b8:e0:80:f6:e5:d4:9f:83:bc:41:89: + 31:97:c8:a4:95:0d:5d:6d:cc:64:8d:19:71:17:75:4b:7f:fb: + 35:88:bf:68:e2:a2:be:c5:71:71:56:2a:92:31:25:2a:4b:98: + 4e:77:42:45:78:45:21:a5:76:99:92:39:32:7d:a2:4c:38:b0: + f1:db:7f:d1:4d:23:99:35:1e:0e:a1:59:a3:ff:9c:51:ef:4c: + 11:c9:32:61:38:11:7d:57:2a:81:9a:96:1f:b3:88:f7:ab:5b: + 58:f7:79:9b:a8:e3:b7:09:90:8e:c9:7d:44:4f:af:85:dc:c8: + 29:4d +-----BEGIN CERTIFICATE----- +MIICfTCCAeagAwIBAgIBATANBgkqhkiG9w0BAQUFADBEMQswCQYDVQQGEwJVUzEL +MAkGA1UECBMCQ0ExFDASBgNVBAoTC00yQ3J5cHRvIENBMRIwEAYDVQQDEwlsb2Nh +bGhvc3QwHhcNMDYwNDIyMDQzNjU2WhcNMDcwNDIyMDQzNjU2WjBIMQswCQYDVQQG +EwJVUzELMAkGA1UECBMCQ0ExGDAWBgNVBAoTD00yQ3J5cHRvIFNlcnZlcjESMBAG +A1UEAxMJbG9jYWxob3N0MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQClzDmt +uoE9vQX0YVCfnPat7CnZeB4kYfcbNr9p2LNFrm86TE/WE21gjfK7KsQbef3i+NY8 +VlM7J/c/cKRkmWNGLj/vUtqpBFtu1EBXxVlh0z99uAPBm2VGKsWdcLfKeW7d5D/C +9C8ugTLI6aa2qMgfSL56ZlaY/Dwl/Nk9cwcwcQIDAQABo3sweTAJBgNVHRMEAjAA +MCwGCWCGSAGG+EIBDQQfFh1PcGVuU1NMIEdlbmVyYXRlZCBDZXJ0aWZpY2F0ZTAd +BgNVHQ4EFgQU8Eg6MoiMgNciq1b4hrMERxB2N74wHwYDVR0jBBgwFoAUXPnFm7AC +Nzxmc00Oy1o9uzpGIt0wDQYJKoZIhvcNAQEFBQADgYEAOUeVXOp+27jggPbl1J+D +vEGJMZfIpJUNXW3MZI0ZcRd1S3/7NYi/aOKivsVxcVYqkjElKkuYTndCRXhFIaV2 +mZI5Mn2iTDiw8dt/0U0jmTUeDqFZo/+cUe9MEckyYTgRfVcqgZqWH7OI96tbWPd5 +m6jjtwmQjsl9RE+vhdzIKU0= +-----END CERTIFICATE----- +-----BEGIN RSA PRIVATE KEY----- +MIICXQIBAAKBgQClzDmtuoE9vQX0YVCfnPat7CnZeB4kYfcbNr9p2LNFrm86TE/W +E21gjfK7KsQbef3i+NY8VlM7J/c/cKRkmWNGLj/vUtqpBFtu1EBXxVlh0z99uAPB +m2VGKsWdcLfKeW7d5D/C9C8ugTLI6aa2qMgfSL56ZlaY/Dwl/Nk9cwcwcQIDAQAB +AoGBAIwzbZbePsnxXOaxoBbJCcQ7D4yJSZvkh6womKauC7Lh9carn1tc5EIg5uCl +Il5Fw466c5dkPE+q1SZ9X1Z+avYh4ypHjpcr1Lfvwo0wfOBx5MBj6ppMdwgrMXuo +jRChK3gBZXmKaIH19h/sGp/lZRW2HiX63aN11KDmtfwo6Bq9AkEA2OftYJUqceK3 +/E8q6OE1oQLm+oK6CPZ29A5TRNOadRu1opDR/y59GcUQ5ebJNH8DXyF82lSi3DKt +SNoSOF32cwJBAMOuKGUAwnq1yH3q+MAF7CZjGou7Ar6VRseyLnD5nttynT85QRW8 +N/WCosKLhV7wi9kKJmHGUJfRAqdo14D8IIsCQQCVjrU6FyABDpZVvjCUClT0BBBH +QsQLUgWLGiWIG28wuD5xLPHexas0jZCtNIgfTkSA35I66Iiy065vwQ03GHLJAkAG +eGC/jjngAtjBSR62grufPVGoYyOhF6CCg+LDO43EJdMLPyJmzJVxGcO1+RUM4ZlO +MOa5/uu1SWT0EiRmEHAnAkBusVcHcd6d4uaoiCybIhF4hL4GsbKoImIciakNlteA +c1RZZHc2jzO/Ihoz50H1njXwY86YbjncOXw8shtayd8j +-----END RSA PRIVATE KEY----- diff --git a/demo/medusa/00_README b/demo/medusa/00_README new file mode 100644 index 0000000..1f0ce36 --- /dev/null +++ b/demo/medusa/00_README @@ -0,0 +1,32 @@ + + 19 Sep 2001 +------------- + +M2Crypto HTTPS and FTP/TLS servers + +All the files in this directory are from the Apr 2001 release +of Medusa, except for the following: + +- 00_README (this file) +- server.pem, the server's certificate +- ca.pem, my CA certificate +- https_server.py +- ftps_server.py +- START.py +- START_xmlrpc.py +- index.html, a sample HTML file +- poison_handler.py, a webpoison clone + +By default, http_server listens on port 9080 and https_server port 9443. +Document root is current directory, and serves up index.html. + +The xmlrpc server is accessible below '/RPC2'. It requires Fredrik Lundh's +xmlrpc_handler on PYTHONPATH. + +The FTP/TLS server listens on port 9021 by default. I've only tested it with +the 'anonymous' authentication handler. + +Medusa files are copyright Sam Rushing. My files are copyright me. + + + diff --git a/demo/medusa/START.py b/demo/medusa/START.py new file mode 100644 index 0000000..62021f7 --- /dev/null +++ b/demo/medusa/START.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python + +# Standard Python library +import os +import os.path +import sys + +# Medusa +import asyncore +import default_handler +import filesys +import ftp_server +import http_server +import status_handler + +# M2Crypto +import https_server +import poison_handler +import ftps_server +from M2Crypto import Rand, SSL, threading + +HTTP_PORT=9080 +HTTPS_PORT=9443 +FTP_PORT = 9021 + +hs=http_server.http_server('', HTTP_PORT) + +Rand.load_file('../randpool.dat', -1) +ssl_ctx=SSL.Context('sslv23') +ssl_ctx.load_cert('server.pem') +ssl_ctx.load_verify_location('ca.pem') +ssl_ctx.load_client_CA('ca.pem') +#ssl_ctx.set_verify(SSL.verify_peer, 10) +#ssl_ctx.set_verify(SSL.verify_peer|SSL.verify_fail_if_no_peer_cert, 10) +#ssl_ctx.set_verify(SSL.verify_peer|SSL.verify_client_once, 10) +ssl_ctx.set_verify(SSL.verify_none, 10) +ssl_ctx.set_session_id_ctx('127.0.0.1:9443') +ssl_ctx.set_tmp_dh('dh1024.pem') +ssl_ctx.set_info_callback() + +hss=https_server.https_server('', HTTPS_PORT, ssl_ctx) + +#fs=filesys.os_filesystem(os.path.abspath(os.curdir)) +fs=filesys.os_filesystem('/usr/local/pkg/apache/htdocs') +#fs=filesys.os_filesystem('c:/pkg/jdk130/docs') +dh=default_handler.default_handler(fs) +hs.install_handler(dh) +hss.install_handler(dh) + +#class rpc_demo (xmlrpc_handler.xmlrpc_handler): +# def call (self, method, params): +# print 'method="%s" params=%s' % (method, params) +# return "Sure, that works" +#rpch = rpc_demo() +#hs.install_handler(rpch) +#hss.install_handler(rpch) + +ph=poison_handler.poison_handler(10) +hs.install_handler(ph) +hss.install_handler(ph) + +fauthz = ftp_server.anon_authorizer('/usr/local/pkg/apache/htdocs') +ftps = ftps_server.ftp_tls_server(fauthz, ssl_ctx, port=FTP_PORT) + +sh=status_handler.status_extension([hs, hss, ftps]) +hs.install_handler(sh) +hss.install_handler(sh) + +asyncore.loop() +Rand.save_file('../randpool.dat') + diff --git a/demo/medusa/START_xmlrpc.py b/demo/medusa/START_xmlrpc.py new file mode 100644 index 0000000..193e8bf --- /dev/null +++ b/demo/medusa/START_xmlrpc.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python + +# Standard Python library +import os +import os.path +import sys + +# Medusa +import asyncore +import default_handler +import filesys +import http_server +import status_handler + +# M2Crypto +import https_server +import poison_handler +from M2Crypto import Rand, SSL + +# XMLrpc +import xmlrpc_handler + + +HTTP_PORT=9080 +HTTPS_PORT=9443 + +hs=http_server.http_server('', HTTP_PORT) + +Rand.load_file('../randpool.dat', -1) +ssl_ctx=SSL.Context('sslv23') +ssl_ctx.load_cert('server.pem') +#ssl_ctx.load_verify_location('ca.pem') +#ssl_ctx.load_client_CA('ca.pem') +#ssl_ctx.set_verify(SSL.verify_peer, 10) +#ssl_ctx.set_verify(SSL.verify_peer|SSL.verify_fail_if_no_peer_cert, 10) +#ssl_ctx.set_verify(SSL.verify_peer|SSL.verify_client_once, 10) +ssl_ctx.set_verify(SSL.verify_none, 10) +ssl_ctx.set_session_id_ctx('127.0.0.1:9443') +ssl_ctx.set_tmp_dh('dh1024.pem') +#ssl_ctx.set_info_callback() + +hss=https_server.https_server('', HTTPS_PORT, ssl_ctx) + +#fs=filesys.os_filesystem(os.path.abspath(os.curdir)) +fs=filesys.os_filesystem('/usr/local/pkg/apache/htdocs') +#fs=filesys.os_filesystem('c:/pkg/jdk118/docs') +dh=default_handler.default_handler(fs) +hs.install_handler(dh) +hss.install_handler(dh) + +# Cribbed from xmlrpc_handler.py. +# This is where you implement your RPC functionality. +class rpc_demo (xmlrpc_handler.xmlrpc_handler): + def call (self, method, params): + print 'method="%s" params=%s' % (method, params) + return "Sure, that works" + +rpch = rpc_demo() +hs.install_handler(rpch) +hss.install_handler(rpch) + +ph=poison_handler.poison_handler(10) +hs.install_handler(ph) +hss.install_handler(ph) + +sh=status_handler.status_extension([hss]) +hs.install_handler(sh) +hss.install_handler(sh) + +asyncore.loop() +Rand.save_file('../randpool.dat') + diff --git a/demo/medusa/asynchat.py b/demo/medusa/asynchat.py new file mode 100644 index 0000000..2e51f1a --- /dev/null +++ b/demo/medusa/asynchat.py @@ -0,0 +1,292 @@ +# -*- Mode: Python; tab-width: 4 -*- +# $Id: asynchat.py 299 2005-06-09 17:32:28Z heikki $ +# Author: Sam Rushing <rushing@nightmare.com> + +# ====================================================================== +# Copyright 1996 by Sam Rushing +# +# All Rights Reserved +# +# Permission to use, copy, modify, and distribute this software and +# its documentation for any purpose and without fee is hereby +# granted, provided that the above copyright notice appear in all +# copies and that both that copyright notice and this permission +# notice appear in supporting documentation, and that the name of Sam +# Rushing not be used in advertising or publicity pertaining to +# distribution of the software without specific, written prior +# permission. +# +# SAM RUSHING DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, +# INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN +# NO EVENT SHALL SAM RUSHING BE LIABLE FOR ANY SPECIAL, INDIRECT OR +# CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +# OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, +# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +# ====================================================================== + +"""A class supporting chat-style (command/response) protocols. + +This class adds support for 'chat' style protocols - where one side +sends a 'command', and the other sends a response (examples would be +the common internet protocols - smtp, nntp, ftp, etc..). + +The handle_read() method looks at the input stream for the current +'terminator' (usually '\r\n' for single-line responses, '\r\n.\r\n' +for multi-line output), calling self.found_terminator() on its +receipt. + +for example: +Say you build an async nntp client using this class. At the start +of the connection, you'll have self.terminator set to '\r\n', in +order to process the single-line greeting. Just before issuing a +'LIST' command you'll set it to '\r\n.\r\n'. The output of the LIST +command will be accumulated (using your own 'collect_incoming_data' +method) up to the terminator, and then control will be returned to +you - by calling your self.found_terminator() method. +""" + +import socket +import asyncore +import string + +class async_chat (asyncore.dispatcher): + """This is an abstract class. You must derive from this class, and add + the two methods collect_incoming_data() and found_terminator()""" + + # these are overridable defaults + + ac_in_buffer_size = 4096 + ac_out_buffer_size = 4096 + + def __init__ (self, conn=None): + self.ac_in_buffer = '' + self.ac_out_buffer = '' + self.producer_fifo = fifo() + asyncore.dispatcher.__init__ (self, conn) + + def set_terminator (self, term): + "Set the input delimiter. Can be a fixed string of any length, an integer, or None" + self.terminator = term + + def get_terminator (self): + return self.terminator + + # grab some more data from the socket, + # throw it to the collector method, + # check for the terminator, + # if found, transition to the next state. + + def handle_read (self): + + try: + data = self.recv (self.ac_in_buffer_size) + except socket.error, why: + self.handle_error() + return + + self.ac_in_buffer = self.ac_in_buffer + data + + # Continue to search for self.terminator in self.ac_in_buffer, + # while calling self.collect_incoming_data. The while loop + # is necessary because we might read several data+terminator + # combos with a single recv(1024). + + while self.ac_in_buffer: + lb = len(self.ac_in_buffer) + terminator = self.get_terminator() + if terminator is None: + # no terminator, collect it all + self.collect_incoming_data (self.ac_in_buffer) + self.ac_in_buffer = '' + elif type(terminator) == type(0): + # numeric terminator + n = terminator + if lb < n: + self.collect_incoming_data (self.ac_in_buffer) + self.ac_in_buffer = '' + self.terminator = self.terminator - lb + else: + self.collect_incoming_data (self.ac_in_buffer[:n]) + self.ac_in_buffer = self.ac_in_buffer[n:] + self.terminator = 0 + self.found_terminator() + else: + # 3 cases: + # 1) end of buffer matches terminator exactly: + # collect data, transition + # 2) end of buffer matches some prefix: + # collect data to the prefix + # 3) end of buffer does not match any prefix: + # collect data + terminator_len = len(terminator) + index = string.find (self.ac_in_buffer, terminator) + if index != -1: + # we found the terminator + if index > 0: + # don't bother reporting the empty string (source of subtle bugs) + self.collect_incoming_data (self.ac_in_buffer[:index]) + self.ac_in_buffer = self.ac_in_buffer[index+terminator_len:] + # This does the Right Thing if the terminator is changed here. + self.found_terminator() + else: + # check for a prefix of the terminator + index = find_prefix_at_end (self.ac_in_buffer, terminator) + if index: + if index != lb: + # we found a prefix, collect up to the prefix + self.collect_incoming_data (self.ac_in_buffer[:-index]) + self.ac_in_buffer = self.ac_in_buffer[-index:] + break + else: + # no prefix, collect it all + self.collect_incoming_data (self.ac_in_buffer) + self.ac_in_buffer = '' + + def handle_write (self): + self.initiate_send () + + def handle_close (self): + self.close() + + def push (self, data): + self.producer_fifo.push (simple_producer (data)) + self.initiate_send() + + def push_with_producer (self, producer): + self.producer_fifo.push (producer) + self.initiate_send() + + def readable (self): + "predicate for inclusion in the readable for select()" + return (len(self.ac_in_buffer) <= self.ac_in_buffer_size) + + def writable (self): + "predicate for inclusion in the writable for select()" + # return len(self.ac_out_buffer) or len(self.producer_fifo) or (not self.connected) + # this is about twice as fast, though not as clear. + return not ( + (self.ac_out_buffer is '') and + self.producer_fifo.is_empty() and + self.connected + ) + + def close_when_done (self): + "automatically close this channel once the outgoing queue is empty" + self.producer_fifo.push (None) + + # refill the outgoing buffer by calling the more() method + # of the first producer in the queue + def refill_buffer (self): + _string_type = type('') + while 1: + if len(self.producer_fifo): + p = self.producer_fifo.first() + # a 'None' in the producer fifo is a sentinel, + # telling us to close the channel. + if p is None: + if not self.ac_out_buffer: + self.producer_fifo.pop() + self.close() + return + elif type(p) is _string_type: + self.producer_fifo.pop() + self.ac_out_buffer = self.ac_out_buffer + p + return + data = p.more() + if data: + self.ac_out_buffer = self.ac_out_buffer + data + return + else: + self.producer_fifo.pop() + else: + return + + def initiate_send (self): + obs = self.ac_out_buffer_size + # try to refill the buffer + if (len (self.ac_out_buffer) < obs): + self.refill_buffer() + + if self.ac_out_buffer and self.connected: + # try to send the buffer + try: + num_sent = self.send (self.ac_out_buffer[:obs]) + if num_sent: + self.ac_out_buffer = self.ac_out_buffer[num_sent:] + + except socket.error, why: + self.handle_error() + return + + def discard_buffers (self): + # Emergencies only! + self.ac_in_buffer = '' + self.ac_out_buffer = '' + while self.producer_fifo: + self.producer_fifo.pop() + + +class simple_producer: + + def __init__ (self, data, buffer_size=512): + self.data = data + self.buffer_size = buffer_size + + def more (self): + if len (self.data) > self.buffer_size: + result = self.data[:self.buffer_size] + self.data = self.data[self.buffer_size:] + return result + else: + result = self.data + self.data = '' + return result + +class fifo: + def __init__ (self, list=None): + if not list: + self.list = [] + else: + self.list = list + + def __len__ (self): + return len(self.list) + + def is_empty (self): + return self.list == [] + + def first (self): + return self.list[0] + + def push (self, data): + self.list.append (data) + + def pop (self): + if self.list: + result = self.list[0] + del self.list[0] + return (1, result) + else: + return (0, None) + +# Given 'haystack', see if any prefix of 'needle' is at its end. This +# assumes an exact match has already been checked. Return the number of +# characters matched. +# for example: +# f_p_a_e ("qwerty\r", "\r\n") => 1 +# f_p_a_e ("qwertydkjf", "\r\n") => 0 +# f_p_a_e ("qwerty\r\n", "\r\n") => <undefined> + +# this could maybe be made faster with a computed regex? +# [answer: no; circa Python-2.0, Jan 2001] +# new python: 28961/s +# old python: 18307/s +# re: 12820/s +# regex: 14035/s + +def find_prefix_at_end (haystack, needle): + l = len(needle) - 1 + while l and not haystack.endswith(needle[:l]): + l -= 1 + return l diff --git a/demo/medusa/asyncore.py b/demo/medusa/asyncore.py new file mode 100644 index 0000000..7f751aa --- /dev/null +++ b/demo/medusa/asyncore.py @@ -0,0 +1,552 @@ +# -*- Mode: Python; tab-width: 4 -*- +# $Id: asyncore.py 299 2005-06-09 17:32:28Z heikki $ +# Author: Sam Rushing <rushing@nightmare.com> + +# ====================================================================== +# Copyright 1996 by Sam Rushing +# +# All Rights Reserved +# +# Permission to use, copy, modify, and distribute this software and +# its documentation for any purpose and without fee is hereby +# granted, provided that the above copyright notice appear in all +# copies and that both that copyright notice and this permission +# notice appear in supporting documentation, and that the name of Sam +# Rushing not be used in advertising or publicity pertaining to +# distribution of the software without specific, written prior +# permission. +# +# SAM RUSHING DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, +# INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN +# NO EVENT SHALL SAM RUSHING BE LIABLE FOR ANY SPECIAL, INDIRECT OR +# CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +# OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, +# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +# ====================================================================== + +"""Basic infrastructure for asynchronous socket service clients and servers. + +There are only two ways to have a program on a single processor do "more +than one thing at a time". Multi-threaded programming is the simplest and +most popular way to do it, but there is another very different technique, +that lets you have nearly all the advantages of multi-threading, without +actually using multiple threads. it's really only practical if your program +is largely I/O bound. If your program is CPU bound, then pre-emptive +scheduled threads are probably what you really need. Network servers are +rarely CPU-bound, however. + +If your operating system supports the select() system call in its I/O +library (and nearly all do), then you can use it to juggle multiple +communication channels at once; doing other work while your I/O is taking +place in the "background." Although this strategy can seem strange and +complex, especially at first, it is in many ways easier to understand and +control than multi-threaded programming. The module documented here solves +many of the difficult problems for you, making the task of building +sophisticated high-performance network servers and clients a snap. +""" + +import exceptions +import select +import socket +import string +import sys + +import os +if os.name == 'nt': + EWOULDBLOCK = 10035 + EINPROGRESS = 10036 + EALREADY = 10037 + ECONNRESET = 10054 + ENOTCONN = 10057 + ESHUTDOWN = 10058 +else: + from errno import EALREADY, EINPROGRESS, EWOULDBLOCK, ECONNRESET, ENOTCONN, ESHUTDOWN + +try: + socket_map +except NameError: + socket_map = {} + +class ExitNow (exceptions.Exception): + pass + +DEBUG = 0 + +def poll (timeout=0.0, map=None): + global DEBUG + if map is None: + map = socket_map + if map: + r = []; w = []; e = [] + for fd, obj in map.items(): + if obj.readable(): + r.append (fd) + if obj.writable(): + w.append (fd) + r,w,e = select.select (r,w,e, timeout) + + if DEBUG: + print r,w,e + + for fd in r: + try: + obj = map[fd] + try: + obj.handle_read_event() + except ExitNow: + raise ExitNow + except: + obj.handle_error() + except KeyError: + pass + + for fd in w: + try: + obj = map[fd] + try: + obj.handle_write_event() + except ExitNow: + raise ExitNow + except: + obj.handle_error() + except KeyError: + pass + +def poll2 (timeout=0.0, map=None): + import poll + if map is None: + map=socket_map + # timeout is in milliseconds + timeout = int(timeout*1000) + if map: + l = [] + for fd, obj in map.items(): + flags = 0 + if obj.readable(): + flags = poll.POLLIN + if obj.writable(): + flags = flags | poll.POLLOUT + if flags: + l.append ((fd, flags)) + r = poll.poll (l, timeout) + for fd, flags in r: + try: + obj = map[fd] + try: + if (flags & poll.POLLIN): + obj.handle_read_event() + if (flags & poll.POLLOUT): + obj.handle_write_event() + except ExitNow: + raise ExitNow + except: + obj.handle_error() + except KeyError: + pass + +def poll3 (timeout=0.0, map=None): + # Use the poll() support added to the select module in Python 2.0 + if map is None: + map=socket_map + # timeout is in milliseconds + timeout = int(timeout*1000) + pollster = select.poll() + if map: + l = [] + for fd, obj in map.items(): + flags = 0 + if obj.readable(): + flags = select.POLLIN + if obj.writable(): + flags = flags | select.POLLOUT + if flags: + pollster.register(fd, flags) + r = pollster.poll (timeout) + for fd, flags in r: + try: + obj = map[fd] + try: + if (flags & select.POLLIN): + obj.handle_read_event() + if (flags & select.POLLOUT): + obj.handle_write_event() + except ExitNow: + raise ExitNow + except: + obj.handle_error() + except KeyError: + pass + +def loop (timeout=30.0, use_poll=0, map=None): + + if use_poll: + if hasattr (select, 'poll'): + poll_fun = poll3 + else: + poll_fun = poll2 + else: + poll_fun = poll + + if map is None: + map=socket_map + + while map: + poll_fun (timeout, map) + +class dispatcher: + debug = 0 + connected = 0 + accepting = 0 + closing = 0 + addr = None + + def __init__ (self, sock=None, map=None): + if sock: + self.set_socket (sock, map) + # I think it should inherit this anyway + self.socket.setblocking (0) + self.connected = 1 + + def __repr__ (self): + try: + status = [] + if self.accepting and self.addr: + status.append ('listening') + elif self.connected: + status.append ('connected') + if self.addr: + status.append ('%s:%d' % self.addr) + return '<%s %s at %x>' % ( + self.__class__.__name__, + string.join (status, ' '), + id(self) + ) + except: + try: + ar = repr(self.addr) + except: + ar = 'no self.addr!' + + return '<__repr__ (self) failed for object at %x (addr=%s)>' % (id(self),ar) + + def add_channel (self, map=None): + #self.log_info ('adding channel %s' % self) + if map is None: + map=socket_map + map [self._fileno] = self + + def del_channel (self, map=None): + fd = self._fileno + if map is None: + map=socket_map + if map.has_key (fd): + #self.log_info ('closing channel %d:%s' % (fd, self)) + del map [fd] + + def create_socket (self, family, type): + self.family_and_type = family, type + self.socket = socket.socket (family, type) + self.socket.setblocking(0) + self._fileno = self.socket.fileno() + self.add_channel() + + def set_socket (self, sock, map=None): + self.__dict__['socket'] = sock + self._fileno = sock.fileno() + self.add_channel (map) + + def set_reuse_addr (self): + # try to re-use a server port if possible + try: + self.socket.setsockopt ( + socket.SOL_SOCKET, socket.SO_REUSEADDR, + self.socket.getsockopt (socket.SOL_SOCKET, socket.SO_REUSEADDR) | 1 + ) + except: + pass + + # ================================================== + # predicates for select() + # these are used as filters for the lists of sockets + # to pass to select(). + # ================================================== + + def readable (self): + return 1 + + if os.name == 'mac': + # The macintosh will select a listening socket for + # write if you let it. What might this mean? + def writable (self): + return not self.accepting + else: + def writable (self): + return 1 + + # ================================================== + # socket object methods. + # ================================================== + + def listen (self, num): + self.accepting = 1 + if os.name == 'nt' and num > 5: + num = 1 + return self.socket.listen (num) + + def bind (self, addr): + self.addr = addr + return self.socket.bind (addr) + + def connect (self, address): + self.connected = 0 + try: + self.socket.connect (address) + except socket.error, why: + if why[0] in (EINPROGRESS, EALREADY, EWOULDBLOCK): + return + else: + raise socket.error, why + self.connected = 1 + self.handle_connect() + + def accept (self): + try: + conn, addr = self.socket.accept() + return conn, addr + except socket.error, why: + if why[0] == EWOULDBLOCK: + pass + else: + raise socket.error, why + + def send (self, data): + try: + result = self.socket.send (data) + return result + except socket.error, why: + if why[0] == EWOULDBLOCK: + return 0 + else: + raise socket.error, why + return 0 + + def recv (self, buffer_size): + try: + data = self.socket.recv (buffer_size) + if not data: + # a closed connection is indicated by signaling + # a read condition, and having recv() return 0. + self.handle_close() + return '' + else: + return data + except socket.error, why: + # winsock sometimes throws ENOTCONN + if why[0] in [ECONNRESET, ENOTCONN, ESHUTDOWN]: + self.handle_close() + return '' + else: + raise socket.error, why + + def close (self): + self.del_channel() + self.socket.close() + + # cheap inheritance, used to pass all other attribute + # references to the underlying socket object. + def __getattr__ (self, attr): + return getattr (self.socket, attr) + + # log and log_info maybe overriden to provide more sophisitcated + # logging and warning methods. In general, log is for 'hit' logging + # and 'log_info' is for informational, warning and error logging. + + def log (self, message): + sys.stderr.write ('log: %s\n' % str(message)) + + def log_info (self, message, type='info'): + if __debug__ or type != 'info': + print '%s: %s' % (type, message) + + def handle_read_event (self): + if self.accepting: + # for an accepting socket, getting a read implies + # that we are connected + if not self.connected: + self.connected = 1 + self.handle_accept() + elif not self.connected: + self.handle_connect() + self.connected = 1 + self.handle_read() + else: + self.handle_read() + + def handle_write_event (self): + # getting a write implies that we are connected + if not self.connected: + self.handle_connect() + self.connected = 1 + self.handle_write() + + def handle_expt_event (self): + self.handle_expt() + + def handle_error (self): + (file,fun,line), t, v, tbinfo = compact_traceback() + + # sometimes a user repr method will crash. + try: + self_repr = repr (self) + except: + self_repr = '<__repr__ (self) failed for object at %0x>' % id(self) + + self.log_info ( + 'uncaptured python exception, closing channel %s (%s:%s %s)' % ( + self_repr, + t, + v, + tbinfo + ), + 'error' + ) + self.close() + + def handle_expt (self): + self.log_info ('unhandled exception', 'warning') + + def handle_read (self): + self.log_info ('unhandled read event', 'warning') + + def handle_write (self): + self.log_info ('unhandled write event', 'warning') + + def handle_connect (self): + self.log_info ('unhandled connect event', 'warning') + + def handle_accept (self): + self.log_info ('unhandled accept event', 'warning') + + def handle_close (self): + self.log_info ('unhandled close event', 'warning') + self.close() + +# --------------------------------------------------------------------------- +# adds simple buffered output capability, useful for simple clients. +# [for more sophisticated usage use asynchat.async_chat] +# --------------------------------------------------------------------------- + +class dispatcher_with_send (dispatcher): + def __init__ (self, sock=None): + dispatcher.__init__ (self, sock) + self.out_buffer = '' + + def initiate_send (self): + num_sent = 0 + num_sent = dispatcher.send (self, self.out_buffer[:512]) + self.out_buffer = self.out_buffer[num_sent:] + + def handle_write (self): + self.initiate_send() + + def writable (self): + return (not self.connected) or len(self.out_buffer) + + def send (self, data): + if self.debug: + self.log_info ('sending %s' % repr(data)) + self.out_buffer = self.out_buffer + data + self.initiate_send() + +# --------------------------------------------------------------------------- +# used for debugging. +# --------------------------------------------------------------------------- + +def compact_traceback (): + t,v,tb = sys.exc_info() + tbinfo = [] + while 1: + tbinfo.append (( + tb.tb_frame.f_code.co_filename, + tb.tb_frame.f_code.co_name, + str(tb.tb_lineno) + )) + tb = tb.tb_next + if not tb: + break + + # just to be safe + del tb + + file, function, line = tbinfo[-1] + info = '[' + string.join ( + map ( + lambda x: string.join (x, '|'), + tbinfo + ), + '] [' + ) + ']' + return (file, function, line), t, v, info + +def close_all (map=None): + if map is None: + map=socket_map + for x in map.values(): + x.socket.close() + map.clear() + +# Asynchronous File I/O: +# +# After a little research (reading man pages on various unixen, and +# digging through the linux kernel), I've determined that select() +# isn't meant for doing doing asynchronous file i/o. +# Heartening, though - reading linux/mm/filemap.c shows that linux +# supports asynchronous read-ahead. So _MOST_ of the time, the data +# will be sitting in memory for us already when we go to read it. +# +# What other OS's (besides NT) support async file i/o? [VMS?] +# +# Regardless, this is useful for pipes, and stdin/stdout... + +import os +if os.name == 'posix': + import fcntl + import FCNTL + + class file_wrapper: + # here we override just enough to make a file + # look like a socket for the purposes of asyncore. + def __init__ (self, fd): + self.fd = fd + + def recv (self, *args): + return apply (os.read, (self.fd,)+args) + + def send (self, *args): + return apply (os.write, (self.fd,)+args) + + read = recv + write = send + + def close (self): + return os.close (self.fd) + + def fileno (self): + return self.fd + + class file_dispatcher (dispatcher): + def __init__ (self, fd): + dispatcher.__init__ (self) + self.connected = 1 + # set it to non-blocking mode + flags = fcntl.fcntl (fd, FCNTL.F_GETFL, 0) + flags = flags | FCNTL.O_NONBLOCK + fcntl.fcntl (fd, FCNTL.F_SETFL, flags) + self.set_file (fd) + + def set_file (self, fd): + self._fileno = fd + self.socket = file_wrapper (fd) + self.add_channel() + diff --git a/demo/medusa/auth_handler.py b/demo/medusa/auth_handler.py new file mode 100644 index 0000000..70eaf3c --- /dev/null +++ b/demo/medusa/auth_handler.py @@ -0,0 +1,135 @@ +# -*- Mode: Python; tab-width: 4 -*- +# +# Author: Sam Rushing <rushing@nightmare.com> +# Copyright 1996-2000 by Sam Rushing +# All Rights Reserved. +# + +# support for 'basic' authenticaion. + +import base64 +import md5 +import re +import string +import time +import counter + +import default_handler + +get_header = default_handler.get_header + +import http_server +import producers + +# This is a 'handler' that wraps an authorization method +# around access to the resources normally served up by +# another handler. + +# does anyone support digest authentication? (rfc2069) + +class auth_handler: + def __init__ (self, dict, handler, realm='default'): + self.authorizer = dictionary_authorizer (dict) + self.handler = handler + self.realm = realm + self.pass_count = counter.counter() + self.fail_count = counter.counter() + + def match (self, request): + # by default, use the given handler's matcher + return self.handler.match (request) + + def handle_request (self, request): + # authorize a request before handling it... + scheme = get_header (AUTHORIZATION, request.header) + + if scheme: + scheme = string.lower (scheme) + if scheme == 'basic': + cookie = AUTHORIZATION.group(2) + try: + decoded = base64.decodestring (cookie) + except: + print 'malformed authorization info <%s>' % cookie + request.error (400) + return + auth_info = string.split (decoded, ':') + if self.authorizer.authorize (auth_info): + self.pass_count.increment() + request.auth_info = auth_info + self.handler.handle_request (request) + else: + self.handle_unauthorized (request) + #elif scheme == 'digest': + # print 'digest: ',AUTHORIZATION.group(2) + else: + print 'unknown/unsupported auth method: %s' % scheme + self.handle_unauthorized() + else: + # list both? prefer one or the other? + # you could also use a 'nonce' here. [see below] + #auth = 'Basic realm="%s" Digest realm="%s"' % (self.realm, self.realm) + #nonce = self.make_nonce (request) + #auth = 'Digest realm="%s" nonce="%s"' % (self.realm, nonce) + #request['WWW-Authenticate'] = auth + #print 'sending header: %s' % request['WWW-Authenticate'] + self.handle_unauthorized (request) + + def handle_unauthorized (self, request): + # We are now going to receive data that we want to ignore. + # to ignore the file data we're not interested in. + self.fail_count.increment() + request.channel.set_terminator (None) + request['Connection'] = 'close' + request['WWW-Authenticate'] = 'Basic realm="%s"' % self.realm + request.error (401) + + def make_nonce (self, request): + "A digest-authentication <nonce>, constructed as suggested in RFC 2069" + ip = request.channel.server.ip + now = str (long (time.time()))[:-1] + private_key = str (id (self)) + nonce = string.join ([ip, now, private_key], ':') + return self.apply_hash (nonce) + + def apply_hash (self, s): + "Apply MD5 to a string <s>, then wrap it in base64 encoding." + m = md5.new() + m.update (s) + d = m.digest() + # base64.encodestring tacks on an extra linefeed. + return base64.encodestring (d)[:-1] + + def status (self): + # Thanks to mwm@contessa.phone.net (Mike Meyer) + r = [ + producers.simple_producer ( + '<li>Authorization Extension : ' + '<b>Unauthorized requests:</b> %s<ul>' % self.fail_count + ) + ] + if hasattr (self.handler, 'status'): + r.append (self.handler.status()) + r.append ( + producers.simple_producer ('</ul>') + ) + return producers.composite_producer ( + http_server.fifo (r) + ) + +class dictionary_authorizer: + def __init__ (self, dict): + self.dict = dict + + def authorize (self, auth_info): + [username, password] = auth_info + if (self.dict.has_key (username)) and (self.dict[username] == password): + return 1 + else: + return 0 + +AUTHORIZATION = re.compile ( + # scheme challenge + 'Authorization: ([^ ]+) (.*)', + re.IGNORECASE + ) diff --git a/demo/medusa/ca.pem b/demo/medusa/ca.pem new file mode 100644 index 0000000..b7c84a1 --- /dev/null +++ b/demo/medusa/ca.pem @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDWTCCAsKgAwIBAgIBADANBgkqhkiG9w0BAQQFADCBgDELMAkGA1UEBhMCU0cx +ETAPBgNVBAoTCE0yQ3J5cHRvMRQwEgYDVQQLEwtNMkNyeXB0byBDQTEkMCIGA1UE +AxMbTTJDcnlwdG8gQ2VydGlmaWNhdGUgTWFzdGVyMSIwIAYJKoZIhvcNAQkBFhNu +Z3BzQG5ldG1lbWV0aWMuY29tMB4XDTAxMTIxNTA1NTU0NloXDTA0MTIxNDA1NTU0 +NlowgYAxCzAJBgNVBAYTAlNHMREwDwYDVQQKEwhNMkNyeXB0bzEUMBIGA1UECxML +TTJDcnlwdG8gQ0ExJDAiBgNVBAMTG00yQ3J5cHRvIENlcnRpZmljYXRlIE1hc3Rl +cjEiMCAGCSqGSIb3DQEJARYTbmdwc0BuZXRtZW1ldGljLmNvbTCBnzANBgkqhkiG +9w0BAQEFAAOBjQAwgYkCgYEAx8soJbS719LHK62VVVIQeC3oW0HvFArwPnA0LuEK +q+LaqMOJg1rS7hvFdX03diV+XJw7cC0iECZYJNG4ii1xbY6KRmufkInaAwm54E3N +e+YYVocaqUkcN6xVf6fwnLfPXbpFS/K2Umg11ObKMmi80JmiIdjcjRRCQZC7g1hf +q+kCAwEAAaOB4DCB3TAdBgNVHQ4EFgQU6/qcBzEtQphfXLhiOHbt2KqBwMIwga0G +A1UdIwSBpTCBooAU6/qcBzEtQphfXLhiOHbt2KqBwMKhgYakgYMwgYAxCzAJBgNV +BAYTAlNHMREwDwYDVQQKEwhNMkNyeXB0bzEUMBIGA1UECxMLTTJDcnlwdG8gQ0Ex +JDAiBgNVBAMTG00yQ3J5cHRvIENlcnRpZmljYXRlIE1hc3RlcjEiMCAGCSqGSIb3 +DQEJARYTbmdwc0BuZXRtZW1ldGljLmNvbYIBADAMBgNVHRMEBTADAQH/MA0GCSqG +SIb3DQEBBAUAA4GBAD+I14GuS5vJmyv1k7mUMbAicsWRHZ+zrGOq9L/L2LsA+lKQ +dAzEZE2+Zv8LBPJVltbJJhcFNJS/ZMAjEm4xlJuCpvXVMxd/M5AM29aqekWlIK7J +vsdDL8IuzpRkMniUiNKPhmB6IPIOslvUKx6QofcE0wDh6pg4VvIbCjkpZ7gf +-----END CERTIFICATE----- diff --git a/demo/medusa/counter.py b/demo/medusa/counter.py new file mode 100644 index 0000000..997d687 --- /dev/null +++ b/demo/medusa/counter.py @@ -0,0 +1,48 @@ +# -*- Mode: Python; tab-width: 4 -*- + +# It is tempting to add an __int__ method to this class, but it's not +# a good idea. This class tries to gracefully handle integer +# overflow, and to hide this detail from both the programmer and the +# user. Note that the __str__ method can be relied on for printing out +# the value of a counter: +# +# >>> print 'Total Client: %s' % self.total_clients +# +# If you need to do arithmetic with the value, then use the 'as_long' +# method, the use of long arithmetic is a reminder that the counter +# will overflow. + +class counter: + "general-purpose counter" + + def __init__ (self, initial_value=0): + self.value = initial_value + + def increment (self, delta=1): + result = self.value + try: + self.value = self.value + delta + except OverflowError: + self.value = long(self.value) + delta + return result + + def decrement (self, delta=1): + result = self.value + try: + self.value = self.value - delta + except OverflowError: + self.value = long(self.value) - delta + return result + + def as_long (self): + return long(self.value) + + def __nonzero__ (self): + return self.value != 0 + + def __repr__ (self): + return '<counter value=%s at %x>' % (self.value, id(self)) + + def __str__ (self): + return str(long(self.value)) + #return str(long(self.value))[:-1] diff --git a/demo/medusa/default_handler.py b/demo/medusa/default_handler.py new file mode 100644 index 0000000..4585b0f --- /dev/null +++ b/demo/medusa/default_handler.py @@ -0,0 +1,215 @@ +# -*- Mode: Python; tab-width: 4 -*- +# +# Author: Sam Rushing <rushing@nightmare.com> +# Copyright 1997 by Sam Rushing +# All Rights Reserved. +# + +# standard python modules +import os +import re +import posixpath +import stat +import string +import time + +# medusa modules +import http_date +import http_server +import mime_type_table +import status_handler +import producers + +unquote = http_server.unquote + +# This is the 'default' handler. it implements the base set of +# features expected of a simple file-delivering HTTP server. file +# services are provided through a 'filesystem' object, the very same +# one used by the FTP server. +# +# You can replace or modify this handler if you want a non-standard +# HTTP server. You can also derive your own handler classes from +# it. +# +# support for handling POST requests is available in the derived +# class <default_with_post_handler>, defined below. +# + +from counter import counter + +class default_handler: + + valid_commands = ['get', 'head'] + + IDENT = 'Default HTTP Request Handler' + + # Pathnames that are tried when a URI resolves to a directory name + directory_defaults = [ + 'index.html', + 'default.html' + ] + + default_file_producer = producers.file_producer + + def __init__ (self, filesystem): + self.filesystem = filesystem + # count total hits + self.hit_counter = counter() + # count file deliveries + self.file_counter = counter() + # count cache hits + self.cache_counter = counter() + + hit_counter = 0 + + def __repr__ (self): + return '<%s (%s hits) at %x>' % ( + self.IDENT, + self.hit_counter, + id (self) + ) + + # always match, since this is a default + def match (self, request): + return 1 + + # handle a file request, with caching. + + def handle_request (self, request): + + if request.command not in self.valid_commands: + request.error (400) # bad request + return + + self.hit_counter.increment() + + path, params, query, fragment = request.split_uri() + + if '%' in path: + path = unquote (path) + + # strip off all leading slashes + while path and path[0] == '/': + path = path[1:] + + if self.filesystem.isdir (path): + if path and path[-1] != '/': + request['Location'] = 'http://%s/%s/' % ( + request.channel.server.server_name, + path + ) + request.error (301) + return + + # we could also generate a directory listing here, + # may want to move this into another method for that + # purpose + found = 0 + if path and path[-1] != '/': + path = path + '/' + for default in self.directory_defaults: + p = path + default + if self.filesystem.isfile (p): + path = p + found = 1 + break + if not found: + request.error (404) # Not Found + return + + elif not self.filesystem.isfile (path): + request.error (404) # Not Found + return + + file_length = self.filesystem.stat (path)[stat.ST_SIZE] + + ims = get_header_match (IF_MODIFIED_SINCE, request.header) + + length_match = 1 + if ims: + length = ims.group (4) + if length: + try: + length = string.atoi (length) + if length != file_length: + length_match = 0 + except: + pass + + ims_date = 0 + + if ims: + ims_date = http_date.parse_http_date (ims.group (1)) + + try: + mtime = self.filesystem.stat (path)[stat.ST_MTIME] + except: + request.error (404) + return + + if length_match and ims_date: + if mtime <= ims_date: + request.reply_code = 304 + request.done() + self.cache_counter.increment() + return + try: + file = self.filesystem.open (path, 'rb') + except IOError: + request.error (404) + return + + request['Last-Modified'] = http_date.build_http_date (mtime) + request['Content-Length'] = file_length + self.set_content_type (path, request) + + if request.command == 'get': + request.push (self.default_file_producer (file)) + + self.file_counter.increment() + request.done() + + def set_content_type (self, path, request): + ext = string.lower (get_extension (path)) + if mime_type_table.content_type_map.has_key (ext): + request['Content-Type'] = mime_type_table.content_type_map[ext] + else: + # TODO: test a chunk off the front of the file for 8-bit + # characters, and use application/octet-stream instead. + request['Content-Type'] = 'text/plain' + + def status (self): + return producers.simple_producer ( + '<li>%s' % status_handler.html_repr (self) + + '<ul>' + + ' <li><b>Total Hits:</b> %s' % self.hit_counter + + ' <li><b>Files Delivered:</b> %s' % self.file_counter + + ' <li><b>Cache Hits:</b> %s' % self.cache_counter + + '</ul>' + ) + +# HTTP/1.0 doesn't say anything about the "; length=nnnn" addition +# to this header. I suppose it's purpose is to avoid the overhead +# of parsing dates... +IF_MODIFIED_SINCE = re.compile ( + 'If-Modified-Since: ([^;]+)((; length=([0-9]+)$)|$)', + re.IGNORECASE + ) + +USER_AGENT = re.compile ('User-Agent: (.*)', re.IGNORECASE) + +CONTENT_TYPE = re.compile ( + r'Content-Type: ([^;]+)((; boundary=([A-Za-z0-9\'\(\)+_,./:=?-]+)$)|$)', + re.IGNORECASE + ) + +get_header = http_server.get_header +get_header_match = http_server.get_header_match + +def get_extension (path): + dirsep = string.rfind (path, '/') + dotsep = string.rfind (path, '.') + if dotsep > dirsep: + return path[dotsep+1:] + else: + return '' diff --git a/demo/medusa/dh1024.pem b/demo/medusa/dh1024.pem new file mode 100644 index 0000000..81d43f6 --- /dev/null +++ b/demo/medusa/dh1024.pem @@ -0,0 +1,5 @@ +-----BEGIN DH PARAMETERS----- +MIGHAoGBAJf2QmHKtQXdKCjhPx1ottPb0PMTBH9A6FbaWMsTuKG/K3g6TG1Z1fkq +/Gz/PWk/eLI9TzFgqVAuPvr3q14a1aZeVUMTgo2oO5/y2UHe6VaJ+trqCTat3xlx +/mNbIK9HA2RgPC3gWfVLZQrY+gz3ASHHR5nXWHEyvpuZm7m3h+irAgEC +-----END DH PARAMETERS----- diff --git a/demo/medusa/filesys.py b/demo/medusa/filesys.py new file mode 100644 index 0000000..003af2e --- /dev/null +++ b/demo/medusa/filesys.py @@ -0,0 +1,466 @@ +# -*- Mode: Python; tab-width: 4 -*- +# $Id: filesys.py 299 2005-06-09 17:32:28Z heikki $ +# Author: Sam Rushing <rushing@nightmare.com> +# +# Generic filesystem interface. +# + +# We want to provide a complete wrapper around any and all +# filesystem operations. + +# this class is really just for documentation, +# identifying the API for a filesystem object. + +# opening files for reading, and listing directories, should +# return a producer. + +class abstract_filesystem: + def __init__ (self): + pass + + def current_directory (self): + "Return a string representing the current directory." + pass + + def listdir (self, path, long=0): + """Return a listing of the directory at 'path' The empty string + indicates the current directory. If 'long' is set, instead + return a list of (name, stat_info) tuples + """ + pass + + def open (self, path, mode): + "Return an open file object" + pass + + def stat (self, path): + "Return the equivalent of os.stat() on the given path." + pass + + def isdir (self, path): + "Does the path represent a directory?" + pass + + def isfile (self, path): + "Does the path represent a plain file?" + pass + + def cwd (self, path): + "Change the working directory." + pass + + def cdup (self): + "Change to the parent of the current directory." + pass + + + def longify (self, path): + """Return a 'long' representation of the filename + [for the output of the LIST command]""" + pass + +# standard wrapper around a unix-like filesystem, with a 'false root' +# capability. + +# security considerations: can symbolic links be used to 'escape' the +# root? should we allow it? if not, then we could scan the +# filesystem on startup, but that would not help if they were added +# later. We will probably need to check for symlinks in the cwd method. + +# what to do if wd is an invalid directory? + +import os +import stat + +import string + +def safe_stat (path): + try: + return (path, os.stat (path)) + except: + return None + +import regsub +import glob + +class os_filesystem: + path_module = os.path + + # set this to zero if you want to disable pathname globbing. + # [we currently don't glob, anyway] + do_globbing = 1 + + def __init__ (self, root, wd='/'): + self.root = root + self.wd = wd + + def current_directory (self): + return self.wd + + def isfile (self, path): + p = self.normalize (self.path_module.join (self.wd, path)) + return self.path_module.isfile (self.translate(p)) + + def isdir (self, path): + p = self.normalize (self.path_module.join (self.wd, path)) + return self.path_module.isdir (self.translate(p)) + + def cwd (self, path): + p = self.normalize (self.path_module.join (self.wd, path)) + translated_path = self.translate(p) + if not self.path_module.isdir (translated_path): + return 0 + else: + old_dir = os.getcwd() + # temporarily change to that directory, in order + # to see if we have permission to do so. + try: + can = 0 + try: + os.chdir (translated_path) + can = 1 + self.wd = p + except: + pass + finally: + if can: + os.chdir (old_dir) + return can + + def cdup (self): + return self.cwd ('..') + + def listdir (self, path, long=0): + p = self.translate (path) + # I think we should glob, but limit it to the current + # directory only. + ld = os.listdir (p) + if not long: + return list_producer (ld, 0, None) + else: + old_dir = os.getcwd() + try: + os.chdir (p) + # if os.stat fails we ignore that file. + result = filter (None, map (safe_stat, ld)) + finally: + os.chdir (old_dir) + return list_producer (result, 1, self.longify) + + # TODO: implement a cache w/timeout for stat() + def stat (self, path): + p = self.translate (path) + return os.stat (p) + + def open (self, path, mode): + p = self.translate (path) + return open (p, mode) + + def unlink (self, path): + p = self.translate (path) + return os.unlink (p) + + def mkdir (self, path): + p = self.translate (path) + return os.mkdir (p) + + def rmdir (self, path): + p = self.translate (path) + return os.rmdir (p) + + # utility methods + def normalize (self, path): + # watch for the ever-sneaky '/+' path element + path = regsub.gsub ('/+', '/', path) + p = self.path_module.normpath (path) + # remove 'dangling' cdup's. + if len(p) > 2 and p[:3] == '/..': + p = '/' + return p + + def translate (self, path): + # we need to join together three separate + # path components, and do it safely. + # <real_root>/<current_directory>/<path> + # use the operating system's path separator. + path = string.join (string.split (path, '/'), os.sep) + p = self.normalize (self.path_module.join (self.wd, path)) + p = self.normalize (self.path_module.join (self.root, p[1:])) + return p + + def longify (self, (path, stat_info)): + return unix_longify (path, stat_info) + + def __repr__ (self): + return '<unix-style fs root:%s wd:%s>' % ( + self.root, + self.wd + ) + +if os.name == 'posix': + + class unix_filesystem (os_filesystem): + pass + + class schizophrenic_unix_filesystem (os_filesystem): + PROCESS_UID = os.getuid() + PROCESS_EUID = os.geteuid() + PROCESS_GID = os.getgid() + PROCESS_EGID = os.getegid() + + def __init__ (self, root, wd='/', persona=(None, None)): + os_filesystem.__init__ (self, root, wd) + self.persona = persona + + def become_persona (self): + if self.persona is not (None, None): + uid, gid = self.persona + # the order of these is important! + os.setegid (gid) + os.seteuid (uid) + + def become_nobody (self): + if self.persona is not (None, None): + os.seteuid (self.PROCESS_UID) + os.setegid (self.PROCESS_GID) + + # cwd, cdup, open, listdir + def cwd (self, path): + try: + self.become_persona() + return os_filesystem.cwd (self, path) + finally: + self.become_nobody() + + def cdup (self, path): + try: + self.become_persona() + return os_filesystem.cdup (self) + finally: + self.become_nobody() + + def open (self, filename, mode): + try: + self.become_persona() + return os_filesystem.open (self, filename, mode) + finally: + self.become_nobody() + + def listdir (self, path, long=0): + try: + self.become_persona() + return os_filesystem.listdir (self, path, long) + finally: + self.become_nobody() + +# This hasn't been very reliable across different platforms. +# maybe think about a separate 'directory server'. +# +# import posixpath +# import fcntl +# import FCNTL +# import select +# import asyncore +# +# # pipes /bin/ls for directory listings. +# class unix_filesystem (os_filesystem): +# pass +# path_module = posixpath +# +# def listdir (self, path, long=0): +# p = self.translate (path) +# if not long: +# return list_producer (os.listdir (p), 0, None) +# else: +# command = '/bin/ls -l %s' % p +# print 'opening pipe to "%s"' % command +# fd = os.popen (command, 'rt') +# return pipe_channel (fd) +# +# # this is both a dispatcher, _and_ a producer +# class pipe_channel (asyncore.file_dispatcher): +# buffer_size = 4096 +# +# def __init__ (self, fd): +# asyncore.file_dispatcher.__init__ (self, fd) +# self.fd = fd +# self.done = 0 +# self.data = '' +# +# def handle_read (self): +# if len (self.data) < self.buffer_size: +# self.data = self.data + self.fd.read (self.buffer_size) +# #print '%s.handle_read() => len(self.data) == %d' % (self, len(self.data)) +# +# def handle_expt (self): +# #print '%s.handle_expt()' % self +# self.done = 1 +# +# def ready (self): +# #print '%s.ready() => %d' % (self, len(self.data)) +# return ((len (self.data) > 0) or self.done) +# +# def more (self): +# if self.data: +# r = self.data +# self.data = '' +# elif self.done: +# self.close() +# self.downstream.finished() +# r = '' +# else: +# r = None +# #print '%s.more() => %s' % (self, (r and len(r))) +# return r + +# For the 'real' root, we could obtain a list of drives, and then +# use that. Doesn't win32 provide such a 'real' filesystem? +# [yes, I think something like this "\\.\c\windows"] + +class msdos_filesystem (os_filesystem): + def longify (self, (path, stat_info)): + return msdos_longify (path, stat_info) + +# A merged filesystem will let you plug other filesystems together. +# We really need the equivalent of a 'mount' capability - this seems +# to be the most general idea. So you'd use a 'mount' method to place +# another filesystem somewhere in the hierarchy. + +# Note: this is most likely how I will handle ~user directories +# with the http server. + +class merged_filesystem: + def __init__ (self, *fsys): + pass + +# this matches the output of NT's ftp server (when in +# MSDOS mode) exactly. + +def msdos_longify (file, stat_info): + if stat.S_ISDIR (stat_info[stat.ST_MODE]): + dir = '<DIR>' + else: + dir = ' ' + date = msdos_date (stat_info[stat.ST_MTIME]) + return '%s %s %8d %s' % ( + date, + dir, + stat_info[stat.ST_SIZE], + file + ) + +def msdos_date (t): + try: + info = time.gmtime (t) + except: + info = time.gmtime (0) + # year, month, day, hour, minute, second, ... + if info[3] > 11: + merid = 'PM' + info[3] = info[3] - 12 + else: + merid = 'AM' + return '%02d-%02d-%02d %02d:%02d%s' % ( + info[1], + info[2], + info[0]%100, + info[3], + info[4], + merid + ) + +months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', + 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] + +mode_table = { + '0':'---', + '1':'--x', + '2':'-w-', + '3':'-wx', + '4':'r--', + '5':'r-x', + '6':'rw-', + '7':'rwx' + } + +import time + +def unix_longify (file, stat_info): + # for now, only pay attention to the lower bits + mode = ('%o' % stat_info[stat.ST_MODE])[-3:] + mode = string.join (map (lambda x: mode_table[x], mode), '') + if stat.S_ISDIR (stat_info[stat.ST_MODE]): + dirchar = 'd' + else: + dirchar = '-' + date = ls_date (long(time.time()), stat_info[stat.ST_MTIME]) + return '%s%s %3d %-8d %-8d %8d %s %s' % ( + dirchar, + mode, + stat_info[stat.ST_NLINK], + stat_info[stat.ST_UID], + stat_info[stat.ST_GID], + stat_info[stat.ST_SIZE], + date, + file + ) + +# Emulate the unix 'ls' command's date field. +# it has two formats - if the date is more than 180 +# days in the past, then it's like this: +# Oct 19 1995 +# otherwise, it looks like this: +# Oct 19 17:33 + +def ls_date (now, t): + try: + info = time.gmtime (t) + except: + info = time.gmtime (0) + # 15,600,000 == 86,400 * 180 + if (now - t) > 15600000: + return '%s %2d %d' % ( + months[info[1]-1], + info[2], + info[0] + ) + else: + return '%s %2d %02d:%02d' % ( + months[info[1]-1], + info[2], + info[3], + info[4] + ) + +# =========================================================================== +# Producers +# =========================================================================== + +class list_producer: + def __init__ (self, file_list, long, longify): + self.file_list = file_list + self.long = long + self.longify = longify + self.done = 0 + + def ready (self): + if len(self.file_list): + return 1 + else: + if not self.done: + self.done = 1 + return 0 + return (len(self.file_list) > 0) + + # this should do a pushd/popd + def more (self): + if not self.file_list: + return '' + else: + # do a few at a time + bunch = self.file_list[:50] + if self.long: + bunch = map (self.longify, bunch) + self.file_list = self.file_list[50:] + return string.joinfields (bunch, '\r\n') + '\r\n' + diff --git a/demo/medusa/ftp_server.py b/demo/medusa/ftp_server.py new file mode 100644 index 0000000..78363cf --- /dev/null +++ b/demo/medusa/ftp_server.py @@ -0,0 +1,1127 @@ +# -*- Mode: Python; tab-width: 4 -*- + +# Author: Sam Rushing <rushing@nightmare.com> +# Copyright 1996-2000 by Sam Rushing +# All Rights Reserved. +# + +# An extensible, configurable, asynchronous FTP server. +# +# All socket I/O is non-blocking, however file I/O is currently +# blocking. Eventually file I/O may be made non-blocking, too, if it +# seems necessary. Currently the only CPU-intensive operation is +# getting and formatting a directory listing. [this could be moved +# into another process/directory server, or another thread?] +# +# Only a subset of RFC 959 is implemented, but much of that RFC is +# vestigial anyway. I've attempted to include the most commonly-used +# commands, using the feature set of wu-ftpd as a guide. + +import asyncore +import asynchat + +import os +import regsub +import socket +import stat +import string +import sys +import time + +# TODO: implement a directory listing cache. On very-high-load +# servers this could save a lot of disk abuse, and possibly the +# work of computing emulated unix ls output. + +# Potential security problem with the FTP protocol? I don't think +# there's any verification of the origin of a data connection. Not +# really a problem for the server (since it doesn't send the port +# command, except when in PASV mode) But I think a data connection +# could be spoofed by a program with access to a sniffer - it could +# watch for a PORT command to go over a command channel, and then +# connect to that port before the server does. + +# Unix user id's: +# In order to support assuming the id of a particular user, +# it seems there are two options: +# 1) fork, and seteuid in the child +# 2) carefully control the effective uid around filesystem accessing +# methods, using try/finally. [this seems to work] + +VERSION = '1.1' + +from counter import counter +import producers +import status_handler +import logger +import string + +class ftp_channel (asynchat.async_chat): + + # defaults for a reliable __repr__ + addr = ('unknown','0') + + # unset this in a derived class in order + # to enable the commands in 'self.write_commands' + read_only = 1 + write_commands = ['appe','dele','mkd','rmd','rnfr','rnto','stor','stou'] + + restart_position = 0 + + # comply with (possibly troublesome) RFC959 requirements + # This is necessary to correctly run an active data connection + # through a firewall that triggers on the source port (expected + # to be 'L-1', or 20 in the normal case). + bind_local_minus_one = 0 + + def __init__ (self, server, conn, addr): + self.server = server + self.current_mode = 'a' + self.addr = addr + asynchat.async_chat.__init__ (self, conn) + self.set_terminator ('\r\n') + + # client data port. Defaults to 'the same as the control connection'. + self.client_addr = (addr[0], 21) + + self.client_dc = None + self.in_buffer = '' + self.closing = 0 + self.passive_acceptor = None + self.passive_connection = None + self.filesystem = None + self.authorized = 0 + # send the greeting + self.respond ( + '220 %s FTP server (Medusa Async V%s [experimental]) ready.' % ( + self.server.hostname, + VERSION + ) + ) + +# def __del__ (self): +# print 'ftp_channel.__del__()' + + # -------------------------------------------------- + # async-library methods + # -------------------------------------------------- + + def handle_expt (self): + # this is handled below. not sure what I could + # do here to make that code less kludgish. + pass + + def collect_incoming_data (self, data): + self.in_buffer = self.in_buffer + data + if len(self.in_buffer) > 4096: + # silently truncate really long lines + # (possible denial-of-service attack) + self.in_buffer = '' + + def found_terminator (self): + + line = self.in_buffer + + if not len(line): + return + + sp = string.find (line, ' ') + if sp != -1: + line = [line[:sp], line[sp+1:]] + else: + line = [line] + + command = string.lower (line[0]) + # watch especially for 'urgent' abort commands. + if string.find (command, 'abor') != -1: + # strip off telnet sync chars and the like... + while command and command[0] not in string.letters: + command = command[1:] + fun_name = 'cmd_%s' % command + if command != 'pass': + self.log ('<== %s' % repr(self.in_buffer)[1:-1]) + else: + self.log ('<== %s' % line[0]+' <password>') + self.in_buffer = '' + if not hasattr (self, fun_name): + self.command_not_understood (line[0]) + return + fun = getattr (self, fun_name) + if (not self.authorized) and (command not in ('user', 'pass', 'help', 'quit')): + self.respond ('530 Please log in with USER and PASS') + elif (not self.check_command_authorization (command)): + self.command_not_authorized (command) + else: + try: + result = apply (fun, (line,)) + except: + self.server.total_exceptions.increment() + (file, fun, line), t,v, tbinfo = asyncore.compact_traceback() + if self.client_dc: + try: + self.client_dc.close() + except: + pass + self.respond ( + '451 Server Error: %s, %s: file: %s line: %s' % ( + t,v,file,line, + ) + ) + + closed = 0 + def close (self): + if not self.closed: + self.closed = 1 + if self.passive_acceptor: + self.passive_acceptor.close() + if self.client_dc: + self.client_dc.close() + self.server.closed_sessions.increment() + asynchat.async_chat.close (self) + + # -------------------------------------------------- + # filesystem interface functions. + # override these to provide access control or perform + # other functions. + # -------------------------------------------------- + + def cwd (self, line): + return self.filesystem.cwd (line[1]) + + def cdup (self, line): + return self.filesystem.cdup() + + def open (self, path, mode): + return self.filesystem.open (path, mode) + + # returns a producer + def listdir (self, path, long=0): + return self.filesystem.listdir (path, long) + + def get_dir_list (self, line, long=0): + # we need to scan the command line for arguments to '/bin/ls'... + args = line[1:] + path_args = [] + for arg in args: + if arg[0] != '-': + path_args.append (arg) + else: + # ignore arguments + pass + if len(path_args) < 1: + dir = '.' + else: + dir = path_args[0] + return self.listdir (dir, long) + + # -------------------------------------------------- + # authorization methods + # -------------------------------------------------- + + def check_command_authorization (self, command): + if command in self.write_commands and self.read_only: + return 0 + else: + return 1 + + # -------------------------------------------------- + # utility methods + # -------------------------------------------------- + + def log (self, message): + self.server.logger.log ( + self.addr[0], + '%d %s' % ( + self.addr[1], message + ) + ) + + def respond (self, resp): + self.log ('==> %s' % resp) + self.push (resp + '\r\n') + + def command_not_understood (self, command): + self.respond ("500 '%s': command not understood." % command) + + def command_not_authorized (self, command): + self.respond ( + "530 You are not authorized to perform the '%s' command" % ( + command + ) + ) + + def make_xmit_channel (self): + # In PASV mode, the connection may or may _not_ have been made + # yet. [although in most cases it is... FTP Explorer being + # the only exception I've yet seen]. This gets somewhat confusing + # because things may happen in any order... + pa = self.passive_acceptor + if pa: + if pa.ready: + # a connection has already been made. + conn, addr = self.passive_acceptor.ready + cdc = xmit_channel (self, addr) + cdc.set_socket (conn) + cdc.connected = 1 + self.passive_acceptor.close() + self.passive_acceptor = None + else: + # we're still waiting for a connect to the PASV port. + cdc = xmit_channel (self) + else: + # not in PASV mode. + ip, port = self.client_addr + cdc = xmit_channel (self, self.client_addr) + cdc.create_socket (socket.AF_INET, socket.SOCK_STREAM) + if self.bind_local_minus_one: + cdc.bind (('', self.server.port - 1)) + try: + cdc.connect ((ip, port)) + except socket.error, why: + self.respond ("425 Can't build data connection") + self.client_dc = cdc + + # pretty much the same as xmit, but only right on the verge of + # being worth a merge. + def make_recv_channel (self, fd): + pa = self.passive_acceptor + if pa: + if pa.ready: + # a connection has already been made. + conn, addr = pa.ready + cdc = recv_channel (self, addr, fd) + cdc.set_socket (conn) + cdc.connected = 1 + self.passive_acceptor.close() + self.passive_acceptor = None + else: + # we're still waiting for a connect to the PASV port. + cdc = recv_channel (self, None, fd) + else: + # not in PASV mode. + ip, port = self.client_addr + cdc = recv_channel (self, self.client_addr, fd) + cdc.create_socket (socket.AF_INET, socket.SOCK_STREAM) + try: + cdc.connect ((ip, port)) + except socket.error, why: + self.respond ("425 Can't build data connection") + self.client_dc = cdc + + type_map = { + 'a':'ASCII', + 'i':'Binary', + 'e':'EBCDIC', + 'l':'Binary' + } + + type_mode_map = { + 'a':'t', + 'i':'b', + 'e':'b', + 'l':'b' + } + + # -------------------------------------------------- + # command methods + # -------------------------------------------------- + + def cmd_type (self, line): + 'specify data transfer type' + # ascii, ebcdic, image, local <byte size> + t = string.lower (line[1]) + # no support for EBCDIC + # if t not in ['a','e','i','l']: + if t not in ['a','i','l']: + self.command_not_understood (string.join (line)) + elif t == 'l' and (len(line) > 2 and line[2] != '8'): + self.respond ('504 Byte size must be 8') + else: + self.current_mode = t + self.respond ('200 Type set to %s.' % self.type_map[t]) + + + def cmd_quit (self, line): + 'terminate session' + self.respond ('221 Goodbye.') + self.close_when_done() + + def cmd_port (self, line): + 'specify data connection port' + info = string.split (line[1], ',') + ip = string.join (info[:4], '.') + port = string.atoi(info[4])*256 + string.atoi(info[5]) + # how many data connections at a time? + # I'm assuming one for now... + # TODO: we should (optionally) verify that the + # ip number belongs to the client. [wu-ftpd does this?] + self.client_addr = (ip, port) + self.respond ('200 PORT command successful.') + + def new_passive_acceptor (self): + # ensure that only one of these exists at a time. + if self.passive_acceptor is not None: + self.passive_acceptor.close() + self.passive_acceptor = None + self.passive_acceptor = passive_acceptor (self) + return self.passive_acceptor + + def cmd_pasv (self, line): + 'prepare for server-to-server transfer' + pc = self.new_passive_acceptor() + port = pc.addr[1] + ip_addr = pc.control_channel.getsockname()[0] + self.respond ( + '227 Entering Passive Mode (%s,%d,%d)' % ( + string.join (string.split (ip_addr, '.'), ','), + port/256, + port%256 + ) + ) + self.client_dc = None + + def cmd_nlst (self, line): + 'give name list of files in directory' + # ncftp adds the -FC argument for the user-visible 'nlist' + # command. We could try to emulate ls flags, but not just yet. + if '-FC' in line: + line.remove ('-FC') + try: + dir_list_producer = self.get_dir_list (line, 0) + except os.error, why: + self.respond ('550 Could not list directory: %s' % repr(why)) + return + self.respond ( + '150 Opening %s mode data connection for file list' % ( + self.type_map[self.current_mode] + ) + ) + self.make_xmit_channel() + self.client_dc.push_with_producer (dir_list_producer) + self.client_dc.close_when_done() + + def cmd_list (self, line): + 'give list files in a directory' + try: + dir_list_producer = self.get_dir_list (line, 1) + except os.error, why: + self.respond ('550 Could not list directory: %s' % repr(why)) + return + self.respond ( + '150 Opening %s mode data connection for file list' % ( + self.type_map[self.current_mode] + ) + ) + self.make_xmit_channel() + self.client_dc.push_with_producer (dir_list_producer) + self.client_dc.close_when_done() + + def cmd_cwd (self, line): + 'change working directory' + if self.cwd (line): + self.respond ('250 CWD command successful.') + else: + self.respond ('550 No such directory.') + + def cmd_cdup (self, line): + 'change to parent of current working directory' + if self.cdup(line): + self.respond ('250 CDUP command successful.') + else: + self.respond ('550 No such directory.') + + def cmd_pwd (self, line): + 'print the current working directory' + self.respond ( + '257 "%s" is the current directory.' % ( + self.filesystem.current_directory() + ) + ) + + # modification time + # example output: + # 213 19960301204320 + def cmd_mdtm (self, line): + 'show last modification time of file' + filename = line[1] + if not self.filesystem.isfile (filename): + self.respond ('550 "%s" is not a file' % filename) + else: + mtime = time.gmtime(self.filesystem.stat(filename)[stat.ST_MTIME]) + self.respond ( + '213 %4d%02d%02d%02d%02d%02d' % ( + mtime[0], + mtime[1], + mtime[2], + mtime[3], + mtime[4], + mtime[5] + ) + ) + + def cmd_noop (self, line): + 'do nothing' + self.respond ('200 NOOP command successful.') + + def cmd_size (self, line): + 'return size of file' + filename = line[1] + if not self.filesystem.isfile (filename): + self.respond ('550 "%s" is not a file' % filename) + else: + self.respond ( + '213 %d' % (self.filesystem.stat(filename)[stat.ST_SIZE]) + ) + + def cmd_retr (self, line): + 'retrieve a file' + if len(line) < 2: + self.command_not_understood (string.join (line)) + else: + file = line[1] + if not self.filesystem.isfile (file): + self.log_info ('checking %s' % file) + self.respond ('550 No such file') + else: + try: + # FIXME: for some reason, 'rt' isn't working on win95 + mode = 'r'+self.type_mode_map[self.current_mode] + fd = self.open (file, mode) + except IOError, why: + self.respond ('553 could not open file for reading: %s' % (repr(why))) + return + self.respond ( + "150 Opening %s mode data connection for file '%s'" % ( + self.type_map[self.current_mode], + file + ) + ) + self.make_xmit_channel() + + if self.restart_position: + # try to position the file as requested, but + # give up silently on failure (the 'file object' + # may not support seek()) + try: + fd.seek (self.restart_position) + except: + pass + self.restart_position = 0 + + self.client_dc.push_with_producer ( + file_producer (self, self.client_dc, fd) + ) + self.client_dc.close_when_done() + + def cmd_stor (self, line, mode='wb'): + 'store a file' + if len (line) < 2: + self.command_not_understood (string.join (line)) + else: + if self.restart_position: + restart_position = 0 + self.respond ('553 restart on STOR not yet supported') + return + file = line[1] + # todo: handle that type flag + try: + fd = self.open (file, mode) + except IOError, why: + self.respond ('553 could not open file for writing: %s' % (repr(why))) + return + self.respond ( + '150 Opening %s connection for %s' % ( + self.type_map[self.current_mode], + file + ) + ) + self.make_recv_channel (fd) + + def cmd_abor (self, line): + 'abort operation' + if self.client_dc: + self.client_dc.close() + self.respond ('226 ABOR command successful.') + + def cmd_appe (self, line): + 'append to a file' + return self.cmd_stor (line, 'ab') + + def cmd_dele (self, line): + if len (line) != 2: + self.command_not_understood (string.join (line)) + else: + file = line[1] + if self.filesystem.isfile (file): + try: + self.filesystem.unlink (file) + self.respond ('250 DELE command successful.') + except: + self.respond ('550 error deleting file.') + else: + self.respond ('550 %s: No such file.' % file) + + def cmd_mkd (self, line): + if len (line) != 2: + self.command.not_understood (string.join (line)) + else: + path = line[1] + try: + self.filesystem.mkdir (path) + self.respond ('257 MKD command successful.') + except: + self.respond ('550 error creating directory.') + + def cmd_rmd (self, line): + if len (line) != 2: + self.command.not_understood (string.join (line)) + else: + path = line[1] + try: + self.filesystem.rmdir (path) + self.respond ('250 RMD command successful.') + except: + self.respond ('550 error removing directory.') + + def cmd_user (self, line): + 'specify user name' + if len(line) > 1: + self.user = line[1] + self.respond ('331 Password required.') + else: + self.command_not_understood (string.join (line)) + + def cmd_pass (self, line): + 'specify password' + if len(line) < 2: + pw = '' + else: + pw = line[1] + result, message, fs = self.server.authorizer.authorize (self, self.user, pw) + if result: + self.respond ('230 %s' % message) + self.filesystem = fs + self.authorized = 1 + self.log_info('Successful login: Filesystem=%s' % repr(fs)) + else: + self.respond ('530 %s' % message) + + def cmd_rest (self, line): + 'restart incomplete transfer' + try: + pos = string.atoi (line[1]) + except ValueError: + self.command_not_understood (string.join (line)) + self.restart_position = pos + self.respond ( + '350 Restarting at %d. Send STORE or RETRIEVE to initiate transfer.' % pos + ) + + def cmd_stru (self, line): + 'obsolete - set file transfer structure' + if line[1] in 'fF': + # f == 'file' + self.respond ('200 STRU F Ok') + else: + self.respond ('504 Unimplemented STRU type') + + def cmd_mode (self, line): + 'obsolete - set file transfer mode' + if line[1] in 'sS': + # f == 'file' + self.respond ('200 MODE S Ok') + else: + self.respond ('502 Unimplemented MODE type') + +# The stat command has two personalities. Normally it returns status +# information about the current connection. But if given an argument, +# it is equivalent to the LIST command, with the data sent over the +# control connection. Strange. But wuftpd, ftpd, and nt's ftp server +# all support it. +# +## def cmd_stat (self, line): +## 'return status of server' +## pass + + def cmd_syst (self, line): + 'show operating system type of server system' + # Replying to this command is of questionable utility, because + # this server does not behave in a predictable way w.r.t. the + # output of the LIST command. We emulate Unix ls output, but + # on win32 the pathname can contain drive information at the front + # Currently, the combination of ensuring that os.sep == '/' + # and removing the leading slash when necessary seems to work. + # [cd'ing to another drive also works] + # + # This is how wuftpd responds, and is probably + # the most expected. The main purpose of this reply is so that + # the client knows to expect Unix ls-style LIST output. + self.respond ('215 UNIX Type: L8') + # one disadvantage to this is that some client programs + # assume they can pass args to /bin/ls. + # a few typical responses: + # 215 UNIX Type: L8 (wuftpd) + # 215 Windows_NT version 3.51 + # 215 VMS MultiNet V3.3 + # 500 'SYST': command not understood. (SVR4) + + def cmd_help (self, line): + 'give help information' + # find all the methods that match 'cmd_xxxx', + # use their docstrings for the help response. + attrs = dir(self.__class__) + help_lines = [] + for attr in attrs: + if attr[:4] == 'cmd_': + x = getattr (self, attr) + if type(x) == type(self.cmd_help): + if x.__doc__: + help_lines.append ('\t%s\t%s' % (attr[4:], x.__doc__)) + if help_lines: + self.push ('214-The following commands are recognized\r\n') + self.push_with_producer (producers.lines_producer (help_lines)) + self.push ('214\r\n') + else: + self.push ('214-\r\n\tHelp Unavailable\r\n214\r\n') + +class ftp_server (asyncore.dispatcher): + # override this to spawn a different FTP channel class. + ftp_channel_class = ftp_channel + + SERVER_IDENT = 'FTP Server (V%s)' % VERSION + + def __init__ ( + self, + authorizer, + hostname =None, + ip ='', + port =21, + resolver =None, + logger_object=logger.file_logger (sys.stdout) + ): + self.ip = ip + self.port = port + self.authorizer = authorizer + + if hostname is None: + self.hostname = socket.gethostname() + else: + self.hostname = hostname + + # statistics + self.total_sessions = counter() + self.closed_sessions = counter() + self.total_files_out = counter() + self.total_files_in = counter() + self.total_bytes_out = counter() + self.total_bytes_in = counter() + self.total_exceptions = counter() + # + asyncore.dispatcher.__init__ (self) + self.create_socket (socket.AF_INET, socket.SOCK_STREAM) + + self.set_reuse_addr() + self.bind ((self.ip, self.port)) + self.listen (5) + + if not logger_object: + logger_object = sys.stdout + + if resolver: + self.logger = logger.resolving_logger (resolver, logger_object) + else: + self.logger = logger.unresolving_logger (logger_object) + + self.log_info('FTP server started at %s\n\tAuthorizer:%s\n\tHostname: %s\n\tPort: %d' % ( + time.ctime(time.time()), + repr (self.authorizer), + self.hostname, + self.port) + ) + + def writable (self): + return 0 + + def handle_read (self): + pass + + def handle_connect (self): + pass + + def handle_accept (self): + conn, addr = self.accept() + self.total_sessions.increment() + self.log_info('Incoming connection from %s:%d' % (addr[0], addr[1])) + self.ftp_channel_class (self, conn, addr) + + # return a producer describing the state of the server + def status (self): + + def nice_bytes (n): + return string.join (status_handler.english_bytes (n)) + + return producers.lines_producer ( + ['<h2>%s</h2>' % self.SERVER_IDENT, + '<br>Listening on <b>Host:</b> %s' % self.hostname, + '<b>Port:</b> %d' % self.port, + '<br>Sessions', + '<b>Total:</b> %s' % self.total_sessions, + '<b>Current:</b> %d' % (self.total_sessions.as_long() - self.closed_sessions.as_long()), + '<br>Files', + '<b>Sent:</b> %s' % self.total_files_out, + '<b>Received:</b> %s' % self.total_files_in, + '<br>Bytes', + '<b>Sent:</b> %s' % nice_bytes (self.total_bytes_out.as_long()), + '<b>Received:</b> %s' % nice_bytes (self.total_bytes_in.as_long()), + '<br>Exceptions: %s' % self.total_exceptions, + ] + ) + +# ====================================================================== +# Data Channel Classes +# ====================================================================== + +# This socket accepts a data connection, used when the server has been +# placed in passive mode. Although the RFC implies that we ought to +# be able to use the same acceptor over and over again, this presents +# a problem: how do we shut it off, so that we are accepting +# connections only when we expect them? [we can't] +# +# wuftpd, and probably all the other servers, solve this by allowing +# only one connection to hit this acceptor. They then close it. Any +# subsequent data-connection command will then try for the default +# port on the client side [which is of course never there]. So the +# 'always-send-PORT/PASV' behavior seems required. +# +# Another note: wuftpd will also be listening on the channel as soon +# as the PASV command is sent. It does not wait for a data command +# first. + +# --- we need to queue up a particular behavior: +# 1) xmit : queue up producer[s] +# 2) recv : the file object +# +# It would be nice if we could make both channels the same. Hmmm.. +# + +class passive_acceptor (asyncore.dispatcher): + ready = None + + def __init__ (self, control_channel): + # connect_fun (conn, addr) + asyncore.dispatcher.__init__ (self) + self.control_channel = control_channel + self.create_socket (socket.AF_INET, socket.SOCK_STREAM) + # bind to an address on the interface that the + # control connection is coming from. + self.bind (( + self.control_channel.getsockname()[0], + 0 + )) + self.addr = self.getsockname() + self.listen (1) + +# def __del__ (self): +# print 'passive_acceptor.__del__()' + + def log (self, *ignore): + pass + + def handle_accept (self): + conn, addr = self.accept() + dc = self.control_channel.client_dc + if dc is not None: + dc.set_socket (conn) + dc.addr = addr + dc.connected = 1 + self.control_channel.passive_acceptor = None + else: + self.ready = conn, addr + self.close() + + +class xmit_channel (asynchat.async_chat): + + # for an ethernet, you want this to be fairly large, in fact, it + # _must_ be large for performance comparable to an ftpd. [64k] we + # ought to investigate automatically-sized buffers... + + ac_out_buffer_size = 16384 + bytes_out = 0 + + def __init__ (self, channel, client_addr=None): + self.channel = channel + self.client_addr = client_addr + asynchat.async_chat.__init__ (self) + +# def __del__ (self): +# print 'xmit_channel.__del__()' + + def log (*args): + pass + + def readable (self): + return not self.connected + + def writable (self): + return 1 + + def send (self, data): + result = asynchat.async_chat.send (self, data) + self.bytes_out = self.bytes_out + result + return result + + def handle_error (self): + # usually this is to catch an unexpected disconnect. + self.log_info ('unexpected disconnect on data xmit channel', 'error') + try: + self.close() + except: + pass + + # TODO: there's a better way to do this. we need to be able to + # put 'events' in the producer fifo. to do this cleanly we need + # to reposition the 'producer' fifo as an 'event' fifo. + + def close (self): + c = self.channel + s = c.server + c.client_dc = None + s.total_files_out.increment() + s.total_bytes_out.increment (self.bytes_out) + if not len(self.producer_fifo): + c.respond ('226 Transfer complete') + elif not c.closed: + c.respond ('426 Connection closed; transfer aborted') + del c + del s + del self.channel + asynchat.async_chat.close (self) + +class recv_channel (asyncore.dispatcher): + def __init__ (self, channel, client_addr, fd): + self.channel = channel + self.client_addr = client_addr + self.fd = fd + asyncore.dispatcher.__init__ (self) + self.bytes_in = counter() + + def log (self, *ignore): + pass + + def handle_connect (self): + pass + + def writable (self): + return 0 + + def recv (*args): + result = apply (asyncore.dispatcher.recv, args) + self = args[0] + self.bytes_in.increment(len(result)) + return result + + buffer_size = 8192 + + def handle_read (self): + block = self.recv (self.buffer_size) + if block: + try: + self.fd.write (block) + except IOError: + self.log_info ('got exception writing block...', 'error') + + def handle_close (self): + s = self.channel.server + s.total_files_in.increment() + s.total_bytes_in.increment(self.bytes_in.as_long()) + self.fd.close() + self.channel.respond ('226 Transfer complete.') + self.close() + +import filesys + +# not much of a doorman! 8^) +class dummy_authorizer: + def __init__ (self, root='/'): + self.root = root + def authorize (self, channel, username, password): + channel.persona = -1, -1 + channel.read_only = 1 + return 1, 'Ok.', filesys.os_filesystem (self.root) + +class anon_authorizer: + def __init__ (self, root='/'): + self.root = root + + def authorize (self, channel, username, password): + if username in ('ftp', 'anonymous'): + channel.persona = -1, -1 + channel.read_only = 1 + return 1, 'Ok.', filesys.os_filesystem (self.root) + else: + return 0, 'Password invalid.', None + +# =========================================================================== +# Unix-specific improvements +# =========================================================================== + +if os.name == 'posix': + + class unix_authorizer: + # return a trio of (success, reply_string, filesystem) + def authorize (self, channel, username, password): + import crypt + import pwd + try: + info = pwd.getpwnam (username) + except KeyError: + return 0, 'No such user.', None + mangled = info[1] + if crypt.crypt (password, mangled[:2]) == mangled: + channel.read_only = 0 + fs = filesys.schizophrenic_unix_filesystem ( + '/', + info[5], + persona = (info[2], info[3]) + ) + return 1, 'Login successful.', fs + else: + return 0, 'Password invalid.', None + + def __repr__ (self): + return '<standard unix authorizer>' + + # simple anonymous ftp support + class unix_authorizer_with_anonymous (unix_authorizer): + def __init__ (self, root=None, real_users=0): + self.root = root + self.real_users = real_users + + def authorize (self, channel, username, password): + if string.lower(username) in ['anonymous', 'ftp']: + import pwd + try: + # ok, here we run into lots of confusion. + # on some os', anon runs under user 'nobody', + # on others as 'ftp'. ownership is also critical. + # need to investigate. + # linux: new linuxen seem to have nobody's UID=-1, + # which is an illegal value. Use ftp. + ftp_user_info = pwd.getpwnam ('ftp') + if string.lower(os.uname()[0]) == 'linux': + nobody_user_info = pwd.getpwnam ('ftp') + else: + nobody_user_info = pwd.getpwnam ('nobody') + channel.read_only = 1 + if self.root is None: + self.root = ftp_user_info[5] + fs = filesys.unix_filesystem (self.root, '/') + return 1, 'Anonymous Login Successful', fs + except KeyError: + return 0, 'Anonymous account not set up', None + elif self.real_users: + return unix_authorizer.authorize ( + self, + channel, + username, + password + ) + else: + return 0, 'User logins not allowed', None + +class file_producer: + block_size = 16384 + def __init__ (self, server, dc, fd): + self.fd = fd + self.done = 0 + + def more (self): + if self.done: + return '' + else: + block = self.fd.read (self.block_size) + if not block: + self.fd.close() + self.done = 1 + return block + +# usage: ftp_server /PATH/TO/FTP/ROOT PORT +# for example: +# $ ftp_server /home/users/ftp 8021 + +if os.name == 'posix': + def test (port='8021'): + import sys + fs = ftp_server ( + unix_authorizer(), + port=string.atoi (port) + ) + try: + asyncore.loop() + except KeyboardInterrupt: + self.log_info('FTP server shutting down. (received SIGINT)', 'warning') + # close everything down on SIGINT. + # of course this should be a cleaner shutdown. + asyncore.close_all() + + if __name__ == '__main__': + test (sys.argv[1]) +# not unix +else: + def test (): + fs = ftp_server (dummy_authorizer()) + if __name__ == '__main__': + test () + +# this is the command list from the wuftpd man page +# '*' means we've implemented it. +# '!' requires write access +# +command_documentation = { + 'abor': 'abort previous command', #* + 'acct': 'specify account (ignored)', + 'allo': 'allocate storage (vacuously)', + 'appe': 'append to a file', #*! + 'cdup': 'change to parent of current working directory', #* + 'cwd': 'change working directory', #* + 'dele': 'delete a file', #! + 'help': 'give help information', #* + 'list': 'give list files in a directory', #* + 'mkd': 'make a directory', #! + 'mdtm': 'show last modification time of file', #* + 'mode': 'specify data transfer mode', + 'nlst': 'give name list of files in directory', #* + 'noop': 'do nothing', #* + 'pass': 'specify password', #* + 'pasv': 'prepare for server-to-server transfer', #* + 'port': 'specify data connection port', #* + 'pwd': 'print the current working directory', #* + 'quit': 'terminate session', #* + 'rest': 'restart incomplete transfer', #* + 'retr': 'retrieve a file', #* + 'rmd': 'remove a directory', #! + 'rnfr': 'specify rename-from file name', #! + 'rnto': 'specify rename-to file name', #! + 'site': 'non-standard commands (see next section)', + 'size': 'return size of file', #* + 'stat': 'return status of server', #* + 'stor': 'store a file', #*! + 'stou': 'store a file with a unique name', #! + 'stru': 'specify data transfer structure', + 'syst': 'show operating system type of server system', #* + 'type': 'specify data transfer type', #* + 'user': 'specify user name', #* + 'xcup': 'change to parent of current working directory (deprecated)', + 'xcwd': 'change working directory (deprecated)', + 'xmkd': 'make a directory (deprecated)', #! + 'xpwd': 'print the current working directory (deprecated)', + 'xrmd': 'remove a directory (deprecated)', #! +} + + +# debugging aid (linux) +def get_vm_size (): + return string.atoi (string.split(open ('/proc/self/stat').readline())[22]) + +def print_vm(): + print 'vm: %8dk' % (get_vm_size()/1024) diff --git a/demo/medusa/ftps_server.py b/demo/medusa/ftps_server.py new file mode 100644 index 0000000..8fee339 --- /dev/null +++ b/demo/medusa/ftps_server.py @@ -0,0 +1,438 @@ +"""An FTP/TLS server built on Medusa's ftp_server. + +Copyright (c) 1999-2003 Ng Pheng Siong. All rights reserved.""" + +# Python +import socket, string, sys, time + +# Medusa +from counter import counter +import asynchat, asyncore, ftp_server, logger + +# M2Crypto +from M2Crypto import SSL + +VERSION_STRING='0.09' + +class ftp_tls_channel(ftp_server.ftp_channel): + + """FTP/TLS server channel for Medusa.""" + + def __init__(self, server, ssl_ctx, conn, addr): + """Initialise the channel.""" + self.ssl_ctx = ssl_ctx + self.server = server + self.current_mode = 'a' + self.addr = addr + asynchat.async_chat.__init__(self, conn) + self.set_terminator('\r\n') + self.client_addr = (addr[0], 21) + self.client_dc = None + self.in_buffer = '' + self.closing = 0 + self.passive_acceptor = None + self.passive_connection = None + self.filesystem = None + self.authorized = 0 + self._ssl_accepting = 0 + self._ssl_accepted = 0 + self._pbsz = None + self._prot = None + resp = '220 %s M2Crypto (Medusa) FTP/TLS server v%s ready.' + self.respond(resp % (self.server.hostname, VERSION_STRING)) + + def writable(self): + return self._ssl_accepting or self._ssl_accepted + + def handle_read(self): + """Handle a read event.""" + if self._ssl_accepting: + self._ssl_accepted = self.socket.accept_ssl() + if self._ssl_accepted: + self._ssl_accepting = 0 + else: + try: + ftp_server.ftp_channel.handle_read(self) + except SSL.SSLError, what: + if str(what) == 'unexpected eof': + self.close() + else: + raise + + def handle_write(self): + """Handle a write event.""" + if self._ssl_accepting: + self._ssl_accepted = self.socket.accept_ssl() + if self._ssl_accepted: + self._ssl_accepting = 0 + else: + try: + ftp_server.ftp_channel.handle_write(self) + except SSL.SSLError, what: + if str(what) == 'unexpected eof': + self.close() + else: + raise + + def send(self, data): + """Send data over SSL.""" + try: + result = self.socket.send(data) + if result <= 0: + return 0 + else: + return result + except SSL.SSLError, what: + self.close() + self.log_info('send: closing channel %s %s' % (repr(self), what)) + return 0 + + def recv(self, buffer_size): + """Receive data over SSL.""" + try: + result = self.socket.recv(buffer_size) + if not result: + return '' + else: + return result + except SSL.SSLError, what: + self.close() + self.log_info('recv: closing channel %s %s' % (repr(self), what)) + return '' + + def found_terminator(self): + """Dispatch the FTP command.""" + line = self.in_buffer + if not len(line): + return + + sp = string.find(line, ' ') + if sp != -1: + line = [line[:sp], line[sp+1:]] + else: + line = [line] + + command = string.lower(line[0]) + if string.find(command, 'stor') != -1: + while command and command[0] not in string.letters: + command = command[1:] + + func_name = 'cmd_%s' % command + if command != 'pass': + self.log('<== %s' % repr(self.in_buffer)[1:-1]) + else: + self.log('<== %s' % line[0]+' <password>') + + self.in_buffer = '' + if not hasattr(self, func_name): + self.command_not_understood(line[0]) + return + + func = getattr(self, func_name) + if not self.check_command_authorization(command): + self.command_not_authorized(command) + else: + try: + result = apply(func, (line,)) + except: + self.server.total_exceptions.increment() + (file, func, line), t, v, tbinfo = asyncore.compact_traceback() + if self.client_dc: + try: + self.client_dc_close() + except: + pass + resp = '451 Server error: %s, %s: file %s line: %s' + self.respond(resp % (t, v, file, line)) + + def make_xmit_channel(self): + """Create a connection for sending data.""" + pa = self.passive_acceptor + if pa: + if pa.ready: + conn, addr = pa.ready + if self._prot: + cdc = tls_xmit_channel(self, conn, self.ssl_ctx, addr) + else: + cdc = ftp_server.xmit_channel(self, addr) + cdc.set_socket(conn) + cdc.connected = 1 + self.passive_acceptor.close() + self.passive_acceptor = None + else: + if self._prot: + cdc = tls_xmit_channel(self, None, self.ssl_ctx, None) + else: + cdc = ftp_server.xmit_channel(self) + else: + if self._prot: + cdc = tls_xmit_channel(self, None, self.ssl_ctx, self.client_addr) + else: + cdc = ftp_server.xmit_channel(self, self.client_addr) + cdc.create_socket(socket.AF_INET, socket.SOCK_STREAM) + if self.bind_local_minus_one: + cdc.bind(('', self.server.port - 1)) + try: + cdc.connect(self.client_addr) + except socket.error, what: + self.respond('425 Cannot build data connection') + self.client_dc = cdc + + def make_recv_channel(self, fd): + """Create a connection for receiving data.""" + pa = self.passive_acceptor + if pa: + if pa.ready: + conn, addr = pa.ready + if self._prot: + cdc = tls_recv_channel(self, conn, self.ssl_ctx, addr, fd) + else: + cdc = ftp_server.recv_channel(self, addr, fd) + cdc.set_socket(conn) + cdc.connected = 1 + self.passive_acceptor.close() + self.passive_acceptor = None + else: + if self._prot: + cdc = tls_recv_channel(self, None, self.ssl_ctx, None, fd) + else: + cdc = ftp_server.recv_channel(self, None, fd) + else: + if self._prot: + cdc = tls_recv_channel(self, None, self.ssl_ctx, self._prot, self.client_addr, fd) + else: + cdc = ftp_server.recv_channel(self, self.client_addr, fd) + cdc.create_socket(socket.AF_INET, socket.SOCK_STREAM) + try: + cdc.connect(self.client_addr) + except socket.error, what: + self.respond('425 Cannot build data connection') + self.client_dc = cdc + + def cmd_auth(self, line): + """Prepare for TLS operation.""" + # XXX Handle variations. + if line[1] != 'TLS': + self.command_not_understood (string.join(line)) + else: + self.respond('234 AUTH TLS successful') + self._ssl_accepting = 1 + self.socket = SSL.Connection(self.ssl_ctx, self.socket) + self.socket.setup_addr(self.addr) + self.socket.setup_ssl() + self.socket.set_accept_state() + self._ssl_accepted = self.socket.accept_ssl() + if self._ssl_accepted: + self._ssl_accepting = 0 + + def cmd_pbsz(self, line): + """Negotiate size of buffer for secure data transfer. For + FTP/TLS the only valid value for the parameter is '0'; any + other value is accepted but ignored.""" + if not (self._ssl_accepting or self._ssl_accepted): + return self.respond('503 AUTH TLS must be issued prior to PBSZ') + self._pbsz = 1 + self.respond('200 PBSZ=0 successful.') + + def cmd_prot(self, line): + """Negotiate the security level of the data connection.""" + if self._pbsz is None: + return self.respond('503 PBSZ must be issued prior to PROT') + if line[1] == 'C': + self.respond('200 Protection set to Clear') + self._pbsz = None + self._prot = None + elif line[1] == 'P': + self.respond('200 Protection set to Private') + self._prot = 1 + elif line[1] in ('S', 'E'): + self.respond('536 PROT %s unsupported' % line[1]) + else: + self.respond('504 PROT %s unsupported' % line[1]) + + +class ftp_tls_server(ftp_server.ftp_server): + + """FTP/TLS server for Medusa.""" + + SERVER_IDENT = 'M2Crypto FTP/TLS Server (v%s)' % VERSION_STRING + + ftp_channel_class = ftp_tls_channel + + def __init__(self, authz, ssl_ctx, host=None, ip='', port=21, resolver=None, log_obj=None): + """Initialise the server.""" + self.ssl_ctx = ssl_ctx + self.ip = ip + self.port = port + self.authorizer = authz + + if host is None: + self.hostname = socket.gethostname() + else: + self.hostname = host + + self.total_sessions = counter() + self.closed_sessions = counter() + self.total_files_out = counter() + self.total_files_in = counter() + self.total_bytes_out = counter() + self.total_bytes_in = counter() + self.total_exceptions = counter() + + asyncore.dispatcher.__init__(self) + self.create_socket(socket.AF_INET, socket.SOCK_STREAM) + self.set_reuse_addr() + self.bind((self.ip, self.port)) + self.listen(5) + + if log_obj is None: + log_obj = sys.stdout + + if resolver: + self.logger = logger.resolving_logger(resolver, log_obj) + else: + self.logger = logger.unresolving_logger(logger.file_logger(sys.stdout)) + + l = 'M2Crypto (Medusa) FTP/TLS server started at %s\n\tAuthz: %s\n\tHostname: %s\n\tPort: %d' + self.log_info(l % (time.ctime(time.time()), repr(self.authorizer), self.hostname, self.port)) + + def handle_accept(self): + """Accept a socket and dispatch a channel to handle it.""" + conn, addr = self.accept() + self.total_sessions.increment() + self.log_info('Connection from %s:%d' % addr) + self.ftp_channel_class(self, self.ssl_ctx, conn, addr) + + +class nbio_ftp_tls_actor: + + """TLS protocol negotiation mixin for FTP/TLS.""" + + def tls_init(self, sock, ssl_ctx, client_addr): + """Perform TLS protocol negotiation.""" + self.ssl_ctx = ssl_ctx + self.client_addr = client_addr + self._ssl_handshaking = 1 + self._ssl_handshake_ok = 0 + if sock: + self.socket = SSL.Connection(self.ssl_ctx, sock) + self.socket.setup_addr(self.client_addr) + self.socket.setup_ssl() + self._ssl_handshake_ok = self.socket.accept_ssl() + if self._ssl_handshake_ok: + self._ssl_handshaking = 0 + self.add_channel() + # else the client hasn't connected yet; when that happens, + # handle_connect() will be triggered. + + def tls_neg_ok(self): + """Return status of TLS protocol negotiation.""" + if self._ssl_handshaking: + self._ssl_handshake_ok = self.socket.accept_ssl() + if self._ssl_handshake_ok: + self._ssl_handshaking = 0 + return self._ssl_handshake_ok + + def handle_connect(self): + """Handle a data connection that occurs after this instance came + into being. When this handler is triggered, self.socket has been + created and refers to the underlying connected socket.""" + self.socket = SSL.Connection(self.ssl_ctx, self.socket) + self.socket.setup_addr(self.client_addr) + self.socket.setup_ssl() + self._ssl_handshake_ok = self.socket.accept_ssl() + if self._ssl_handshake_ok: + self._ssl_handshaking = 0 + self.add_channel() + + def send(self, data): + """Send data over SSL.""" + try: + result = self.socket.send(data) + if result <= 0: + return 0 + else: + return result + except SSL.SSLError, what: + self.close() + self.log_info('send: closing channel %s %s' % (repr(self), what)) + return 0 + + def recv(self, buffer_size): + """Receive data over SSL.""" + try: + result = self.socket.recv(buffer_size) + if not result: + return '' + else: + return result + except SSL.SSLError, what: + self.close() + self.log_info('recv: closing channel %s %s' % (repr(self), what)) + return '' + + +class tls_xmit_channel(nbio_ftp_tls_actor, ftp_server.xmit_channel): + + """TLS driver for a send-only data connection.""" + + def __init__(self, channel, conn, ssl_ctx, client_addr=None): + """Initialise the driver.""" + ftp_server.xmit_channel.__init__(self, channel, client_addr) + self.tls_init(conn, ssl_ctx, client_addr) + + def readable(self): + """This channel is readable iff TLS negotiation is in progress. + (Which implies a connected channel, of course.)""" + if not self.connected: + return 0 + else: + return self._ssl_handshaking + + def writable(self): + """This channel is writable iff TLS negotiation is in progress + or the application has data to send.""" + if self._ssl_handshaking: + return 1 + else: + return ftp_server.xmit_channel.writable(self) + + def handle_read(self): + """Handle a read event: either continue with TLS negotiation + or let the application handle this event.""" + if self.tls_neg_ok(): + ftp_server.xmit_channel.handle_read(self) + + def handle_write(self): + """Handle a write event: either continue with TLS negotiation + or let the application handle this event.""" + if self.tls_neg_ok(): + ftp_server.xmit_channel.handle_write(self) + + +class tls_recv_channel(nbio_ftp_tls_actor, ftp_server.recv_channel): + + """TLS driver for a receive-only data connection.""" + + def __init__(self, channel, conn, ssl_ctx, client_addr, fd): + """Initialise the driver.""" + ftp_server.recv_channel.__init__(self, channel, client_addr, fd) + self.tls_init(conn, ssl_ctx, client_addr) + + def writable(self): + """This channel is writable iff TLS negotiation is in progress.""" + return self._ssl_handshaking + + def handle_read(self): + """Handle a read event: either continue with TLS negotiation + or let the application handle this event.""" + if self.tls_neg_ok(): + ftp_server.recv_channel.handle_read(self) + + def handle_write(self): + """Handle a write event: either continue with TLS negotiation + or let the application handle this event.""" + if self.tls_neg_ok(): + ftp_server.recv_channel.handle_write(self) + + diff --git a/demo/medusa/http_date.py b/demo/medusa/http_date.py new file mode 100644 index 0000000..c40f70d --- /dev/null +++ b/demo/medusa/http_date.py @@ -0,0 +1,126 @@ +# -*- Mode: Python; tab-width: 4 -*- + +import re +import string +import time + +def concat (*args): + return ''.join (args) + +def join (seq, field=' '): + return field.join (seq) + +def group (s): + return '(' + s + ')' + +short_days = ['sun','mon','tue','wed','thu','fri','sat'] +long_days = ['sunday','monday','tuesday','wednesday','thursday','friday','saturday'] + +short_day_reg = group (join (short_days, '|')) +long_day_reg = group (join (long_days, '|')) + +daymap = {} +for i in range(7): + daymap[short_days[i]] = i + daymap[long_days[i]] = i + +hms_reg = join (3 * [group('[0-9][0-9]')], ':') + +months = ['jan','feb','mar','apr','may','jun','jul','aug','sep','oct','nov','dec'] + +monmap = {} +for i in range(12): + monmap[months[i]] = i+1 + +months_reg = group (join (months, '|')) + +# From draft-ietf-http-v11-spec-07.txt/3.3.1 +# Sun, 06 Nov 1994 08:49:37 GMT ; RFC 822, updated by RFC 1123 +# Sunday, 06-Nov-94 08:49:37 GMT ; RFC 850, obsoleted by RFC 1036 +# Sun Nov 6 08:49:37 1994 ; ANSI C's asctime() format + +# rfc822 format +rfc822_date = join ( + [concat (short_day_reg,','), # day + group('[0-9][0-9]?'), # date + months_reg, # month + group('[0-9]+'), # year + hms_reg, # hour minute second + 'gmt' + ], + ' ' + ) + +rfc822_reg = re.compile (rfc822_date) + +def unpack_rfc822 (m): + g = m.group + a = string.atoi + return ( + a(g(4)), # year + monmap[g(3)], # month + a(g(2)), # day + a(g(5)), # hour + a(g(6)), # minute + a(g(7)), # second + 0, + 0, + 0 + ) + +# rfc850 format +rfc850_date = join ( + [concat (long_day_reg,','), + join ( + [group ('[0-9][0-9]?'), + months_reg, + group ('[0-9]+') + ], + '-' + ), + hms_reg, + 'gmt' + ], + ' ' + ) + +rfc850_reg = re.compile (rfc850_date) +# they actually unpack the same way +def unpack_rfc850 (m): + g = m.group + a = string.atoi + return ( + a(g(4)), # year + monmap[g(3)], # month + a(g(2)), # day + a(g(5)), # hour + a(g(6)), # minute + a(g(7)), # second + 0, + 0, + 0 + ) + +# parsdate.parsedate - ~700/sec. +# parse_http_date - ~1333/sec. + +def build_http_date (when): + return time.strftime ('%a, %d %b %Y %H:%M:%S GMT', time.gmtime(when)) + +def parse_http_date (d): + d = string.lower (d) + tz = time.timezone + m = rfc850_reg.match (d) + if m and m.end() == len(d): + retval = int (time.mktime (unpack_rfc850(m)) - tz) + else: + m = rfc822_reg.match (d) + if m and m.end() == len(d): + retval = int (time.mktime (unpack_rfc822(m)) - tz) + else: + return 0 + # Thanks to Craig Silverstein <csilvers@google.com> for pointing + # out the DST discrepancy + if time.daylight and time.localtime(retval)[-1] == 1: # DST correction + retval = retval + (tz - time.altzone) + return retval diff --git a/demo/medusa/http_server.py b/demo/medusa/http_server.py new file mode 100644 index 0000000..2f261ab --- /dev/null +++ b/demo/medusa/http_server.py @@ -0,0 +1,784 @@ +#! /usr/local/bin/python +# -*- Mode: Python; tab-width: 4 -*- +# +# Author: Sam Rushing <rushing@nightmare.com> +# Copyright 1996-2000 by Sam Rushing +# All Rights Reserved. +# + +# python modules +import os +import re +import socket +import stat +import string +import sys +import time + +# async modules +import asyncore +import asynchat + +# medusa modules +import http_date +import producers +import status_handler +import logger + +VERSION_STRING = '1.1' + +from counter import counter +from urllib import unquote + +# =========================================================================== +# Request Object +# =========================================================================== + +class http_request: + + # default reply code + reply_code = 200 + + request_counter = counter() + + # Whether to automatically use chunked encoding when + # + # HTTP version is 1.1 + # Content-Length is not set + # Chunked encoding is not already in effect + # + # If your clients are having trouble, you might want to disable this. + use_chunked = 1 + + # by default, this request object ignores user data. + collector = None + + def __init__ (self, *args): + # unpack information about the request + (self.channel, self.request, + self.command, self.uri, self.version, + self.header) = args + + self.outgoing = fifo() + self.reply_headers = { + 'Server' : 'Medusa/%s' % VERSION_STRING, + 'Date' : http_date.build_http_date (time.time()) + } + self.request_number = http_request.request_counter.increment() + self._split_uri = None + self._header_cache = {} + + # -------------------------------------------------- + # reply header management + # -------------------------------------------------- + def __setitem__ (self, key, value): + self.reply_headers[key] = value + + def __getitem__ (self, key): + return self.reply_headers[key] + + def has_key (self, key): + return self.reply_headers.has_key (key) + + def build_reply_header (self): + return string.join ( + [self.response(self.reply_code)] + map ( + lambda x: '%s: %s' % x, + self.reply_headers.items() + ), + '\r\n' + ) + '\r\n\r\n' + + # -------------------------------------------------- + # split a uri + # -------------------------------------------------- + + # <path>;<params>?<query>#<fragment> + path_regex = re.compile ( + # path params query fragment + r'([^;?#]*)(;[^?#]*)?(\?[^#]*)?(#.*)?' + ) + + def split_uri (self): + if self._split_uri is None: + m = self.path_regex.match (self.uri) + if m.end() != len(self.uri): + raise ValueError, "Broken URI" + else: + self._split_uri = m.groups() + return self._split_uri + + def get_header_with_regex (self, head_reg, group): + for line in self.header: + m = head_reg.match (line) + if m.end() == len(line): + return head_reg.group (group) + return '' + + def get_header (self, header): + header = string.lower (header) + hc = self._header_cache + if not hc.has_key (header): + h = header + ': ' + hl = len(h) + for line in self.header: + if string.lower (line[:hl]) == h: + r = line[hl:] + hc[header] = r + return r + hc[header] = None + return None + else: + return hc[header] + + # -------------------------------------------------- + # user data + # -------------------------------------------------- + + def collect_incoming_data (self, data): + if self.collector: + self.collector.collect_incoming_data (data) + else: + self.log_info( + 'Dropping %d bytes of incoming request data' % len(data), + 'warning' + ) + + def found_terminator (self): + if self.collector: + self.collector.found_terminator() + else: + self.log_info ( + 'Unexpected end-of-record for incoming request', + 'warning' + ) + + def push (self, thing): + if type(thing) == type(''): + self.outgoing.push (producers.simple_producer (thing)) + else: + self.outgoing.push (thing) + + def response (self, code=200): + message = self.responses[code] + self.reply_code = code + return 'HTTP/%s %d %s' % (self.version, code, message) + + def error (self, code): + self.reply_code = code + message = self.responses[code] + s = self.DEFAULT_ERROR_MESSAGE % { + 'code': code, + 'message': message, + } + self['Content-Length'] = len(s) + self['Content-Type'] = 'text/html' + # make an error reply + self.push (s) + self.done() + + # can also be used for empty replies + reply_now = error + + def done (self): + "finalize this transaction - send output to the http channel" + + # ---------------------------------------- + # persistent connection management + # ---------------------------------------- + + # --- BUCKLE UP! ---- + + connection = string.lower (get_header (CONNECTION, self.header)) + + close_it = 0 + wrap_in_chunking = 0 + + if self.version == '1.0': + if connection == 'keep-alive': + if not self.has_key ('Content-Length'): + close_it = 1 + else: + self['Connection'] = 'Keep-Alive' + else: + close_it = 1 + elif self.version == '1.1': + if connection == 'close': + close_it = 1 + elif not self.has_key ('Content-Length'): + if self.has_key ('Transfer-Encoding'): + if not self['Transfer-Encoding'] == 'chunked': + close_it = 1 + elif self.use_chunked: + self['Transfer-Encoding'] = 'chunked' + wrap_in_chunking = 1 + else: + close_it = 1 + elif self.version is None: + # Although we don't *really* support http/0.9 (because we'd have to + # use \r\n as a terminator, and it would just yuck up a lot of stuff) + # it's very common for developers to not want to type a version number + # when using telnet to debug a server. + close_it = 1 + + outgoing_header = producers.simple_producer (self.build_reply_header()) + + if close_it: + self['Connection'] = 'close' + + if wrap_in_chunking: + outgoing_producer = producers.chunked_producer ( + producers.composite_producer (self.outgoing) + ) + # prepend the header + outgoing_producer = producers.composite_producer ( + fifo([outgoing_header, outgoing_producer]) + ) + else: + # prepend the header + self.outgoing.push_front (outgoing_header) + outgoing_producer = producers.composite_producer (self.outgoing) + + # apply a few final transformations to the output + self.channel.push_with_producer ( + # globbing gives us large packets + producers.globbing_producer ( + # hooking lets us log the number of bytes sent + producers.hooked_producer ( + outgoing_producer, + self.log + ) + ) + ) + + self.channel.current_request = None + + if close_it: + self.channel.close_when_done() + + def log_date_string (self, when): + return time.strftime ( + '%d/%b/%Y:%H:%M:%S ', + time.gmtime(when) + ) + tz_for_log + + def log (self, bytes): + self.channel.server.logger.log ( + self.channel.addr[0], + '%d - - [%s] "%s" %d %d\n' % ( + self.channel.addr[1], + self.log_date_string (time.time()), + self.request, + self.reply_code, + bytes + ) + ) + + responses = { + 100: "Continue", + 101: "Switching Protocols", + 200: "OK", + 201: "Created", + 202: "Accepted", + 203: "Non-Authoritative Information", + 204: "No Content", + 205: "Reset Content", + 206: "Partial Content", + 300: "Multiple Choices", + 301: "Moved Permanently", + 302: "Moved Temporarily", + 303: "See Other", + 304: "Not Modified", + 305: "Use Proxy", + 400: "Bad Request", + 401: "Unauthorized", + 402: "Payment Required", + 403: "Forbidden", + 404: "Not Found", + 405: "Method Not Allowed", + 406: "Not Acceptable", + 407: "Proxy Authentication Required", + 408: "Request Time-out", + 409: "Conflict", + 410: "Gone", + 411: "Length Required", + 412: "Precondition Failed", + 413: "Request Entity Too Large", + 414: "Request-URI Too Large", + 415: "Unsupported Media Type", + 500: "Internal Server Error", + 501: "Not Implemented", + 502: "Bad Gateway", + 503: "Service Unavailable", + 504: "Gateway Time-out", + 505: "HTTP Version not supported" + } + + # Default error message + DEFAULT_ERROR_MESSAGE = string.join ( + ['<head>', + '<title>Error response</title>', + '</head>', + '<body>', + '<h1>Error response</h1>', + '<p>Error code %(code)d.', + '<p>Message: %(message)s.', + '</body>', + '' + ], + '\r\n' + ) + + +# =========================================================================== +# HTTP Channel Object +# =========================================================================== + +class http_channel (asynchat.async_chat): + + # use a larger default output buffer + ac_out_buffer_size = 1<<16 + + current_request = None + channel_counter = counter() + + def __init__ (self, server, conn, addr): + self.channel_number = http_channel.channel_counter.increment() + self.request_counter = counter() + asynchat.async_chat.__init__ (self, conn) + self.server = server + self.addr = addr + self.set_terminator ('\r\n\r\n') + self.in_buffer = '' + self.creation_time = int (time.time()) + self.check_maintenance() + + def __repr__ (self): + ar = asynchat.async_chat.__repr__(self)[1:-1] + return '<%s channel#: %s requests:%s>' % ( + ar, + self.channel_number, + self.request_counter + ) + + # Channel Counter, Maintenance Interval... + maintenance_interval = 500 + + def check_maintenance (self): + if not self.channel_number % self.maintenance_interval: + self.maintenance() + + def maintenance (self): + self.kill_zombies() + + # 30-minute zombie timeout. status_handler also knows how to kill zombies. + zombie_timeout = 30 * 60 + + def kill_zombies (self): + now = int (time.time()) + for channel in asyncore.socket_map.values(): + if channel.__class__ == self.__class__: + if (now - channel.creation_time) > channel.zombie_timeout: + channel.close() + + # -------------------------------------------------- + # send/recv overrides, good place for instrumentation. + # -------------------------------------------------- + + # this information needs to get into the request object, + # so that it may log correctly. + def send (self, data): + result = asynchat.async_chat.send (self, data) + self.server.bytes_out.increment (len(data)) + return result + + def recv (self, buffer_size): + try: + result = asynchat.async_chat.recv (self, buffer_size) + self.server.bytes_in.increment (len(result)) + return result + except MemoryError: + # --- Save a Trip to Your Service Provider --- + # It's possible for a process to eat up all the memory of + # the machine, and put it in an extremely wedged state, + # where medusa keeps running and can't be shut down. This + # is where MemoryError tends to get thrown, though of + # course it could get thrown elsewhere. + sys.exit ("Out of Memory!") + + def handle_error (self): + t, v = sys.exc_info()[:2] + if t is SystemExit: + raise t, v + else: + asynchat.async_chat.handle_error (self) + + def log (self, *args): + pass + + # -------------------------------------------------- + # async_chat methods + # -------------------------------------------------- + + def collect_incoming_data (self, data): + if self.current_request: + # we are receiving data (probably POST data) for a request + self.current_request.collect_incoming_data (data) + else: + # we are receiving header (request) data + self.in_buffer = self.in_buffer + data + + def found_terminator (self): + if self.current_request: + self.current_request.found_terminator() + else: + header = self.in_buffer + self.in_buffer = '' + lines = string.split (header, '\r\n') + + # -------------------------------------------------- + # crack the request header + # -------------------------------------------------- + + while lines and not lines[0]: + # as per the suggestion of http-1.1 section 4.1, (and + # Eric Parker <eparker@zyvex.com>), ignore a leading + # blank lines (buggy browsers tack it onto the end of + # POST requests) + lines = lines[1:] + + if not lines: + self.close_when_done() + return + + request = lines[0] + + # unquote path if necessary (thanks to Skip Montaro for pointing + # out that we must unquote in piecemeal fashion). + if '%' in request: + request = unquote (request) + + command, uri, version = crack_request (request) + header = join_headers (lines[1:]) + + r = http_request (self, request, command, uri, version, header) + self.request_counter.increment() + self.server.total_requests.increment() + + if command is None: + self.log_info ('Bad HTTP request: %s' % repr(request), 'error') + r.error (400) + return + + # -------------------------------------------------- + # handler selection and dispatch + # -------------------------------------------------- + for h in self.server.handlers: + if h.match (r): + try: + self.current_request = r + # This isn't used anywhere. + # r.handler = h # CYCLE + h.handle_request (r) + except: + self.server.exceptions.increment() + (file, fun, line), t, v, tbinfo = asyncore.compact_traceback() + self.log_info( + 'Server Error: %s, %s: file: %s line: %s' % (t,v,file,line), + 'error') + try: + r.error (500) + except: + pass + return + + # no handlers, so complain + r.error (404) + + def writable (self): + # this is just the normal async_chat 'writable', here for comparison + return self.ac_out_buffer or len(self.producer_fifo) + + def writable_for_proxy (self): + # this version of writable supports the idea of a 'stalled' producer + # [i.e., it's not ready to produce any output yet] This is needed by + # the proxy, which will be waiting for the magic combination of + # 1) hostname resolved + # 2) connection made + # 3) data available. + if self.ac_out_buffer: + return 1 + elif len(self.producer_fifo): + p = self.producer_fifo.first() + if hasattr (p, 'stalled'): + return not p.stalled() + else: + return 1 + +# =========================================================================== +# HTTP Server Object +# =========================================================================== + +class http_server (asyncore.dispatcher): + + SERVER_IDENT = 'HTTP Server (V%s)' % VERSION_STRING + + channel_class = http_channel + + def __init__ (self, ip, port, resolver=None, logger_object=None): + self.ip = ip + self.port = port + asyncore.dispatcher.__init__ (self) + self.create_socket (socket.AF_INET, socket.SOCK_STREAM) + + self.handlers = [] + + if not logger_object: + logger_object = logger.file_logger (sys.stdout) + + self.set_reuse_addr() + self.bind ((ip, port)) + + # lower this to 5 if your OS complains + self.listen (1024) + + host, port = self.socket.getsockname() + if not ip: + self.log_info('Computing default hostname', 'warning') + ip = socket.gethostbyname (socket.gethostname()) + try: + self.server_name = socket.gethostbyaddr (ip)[0] + except socket.error: + self.log_info('Cannot do reverse lookup', 'warning') + self.server_name = ip # use the IP address as the "hostname" + + self.server_port = port + self.total_clients = counter() + self.total_requests = counter() + self.exceptions = counter() + self.bytes_out = counter() + self.bytes_in = counter() + + if not logger_object: + logger_object = logger.file_logger (sys.stdout) + + if resolver: + self.logger = logger.resolving_logger (resolver, logger_object) + else: + self.logger = logger.unresolving_logger (logger_object) + + self.log_info ( + 'Medusa (V%s) started at %s' + '\n\tHostname: %s' + '\n\tPort:%d' + '\n' % ( + VERSION_STRING, + time.ctime(time.time()), + self.server_name, + port, + ) + ) + + def writable (self): + return 0 + + def handle_read (self): + pass + + def readable (self): + return self.accepting + + def handle_connect (self): + pass + + def handle_accept (self): + self.total_clients.increment() + try: + conn, addr = self.accept() + except socket.error: + # linux: on rare occasions we get a bogus socket back from + # accept. socketmodule.c:makesockaddr complains that the + # address family is unknown. We don't want the whole server + # to shut down because of this. + self.log_info ('warning: server accept() threw an exception', 'warning') + return + except TypeError: + # unpack non-sequence. this can happen when a read event + # fires on a listening socket, but when we call accept() + # we get EWOULDBLOCK, so dispatcher.accept() returns None. + # Seen on FreeBSD3. + self.log_info ('warning: server accept() threw EWOULDBLOCK', 'warning') + return + + self.channel_class (self, conn, addr) + + def install_handler (self, handler, back=0): + if back: + self.handlers.append (handler) + else: + self.handlers.insert (0, handler) + + def remove_handler (self, handler): + self.handlers.remove (handler) + + def status (self): + def nice_bytes (n): + return string.join (status_handler.english_bytes (n)) + + handler_stats = filter (None, map (maybe_status, self.handlers)) + + if self.total_clients: + ratio = self.total_requests.as_long() / float(self.total_clients.as_long()) + else: + ratio = 0.0 + + return producers.composite_producer ( + fifo ([producers.lines_producer ( + ['<h2>%s</h2>' % self.SERVER_IDENT, + '<br>Listening on: <b>Host:</b> %s' % self.server_name, + '<b>Port:</b> %d' % self.port, + '<p><ul>' + '<li>Total <b>Clients:</b> %s' % self.total_clients, + '<b>Requests:</b> %s' % self.total_requests, + '<b>Requests/Client:</b> %.1f' % (ratio), + '<li>Total <b>Bytes In:</b> %s' % (nice_bytes (self.bytes_in.as_long())), + '<b>Bytes Out:</b> %s' % (nice_bytes (self.bytes_out.as_long())), + '<li>Total <b>Exceptions:</b> %s' % self.exceptions, + '</ul><p>' + '<b>Extension List</b><ul>', + ])] + handler_stats + [producers.simple_producer('</ul>')] + ) + ) + +def maybe_status (thing): + if hasattr (thing, 'status'): + return thing.status() + else: + return None + +CONNECTION = re.compile ('Connection: (.*)', re.IGNORECASE) + +# merge multi-line headers +# [486dx2: ~500/sec] +def join_headers (headers): + r = [] + for i in range(len(headers)): + if headers[i][0] in ' \t': + r[-1] = r[-1] + headers[i][1:] + else: + r.append (headers[i]) + return r + +def get_header (head_reg, lines, group=1): + for line in lines: + m = head_reg.match (line) + if m and m.end() == len(line): + return m.group (group) + return '' + +def get_header_match (head_reg, lines): + for line in lines: + m = head_reg.match (line) + if m and m.end() == len(line): + return m + return '' + +REQUEST = re.compile ('([^ ]+) ([^ ]+)(( HTTP/([0-9.]+))$|$)') + +def crack_request (r): + m = REQUEST.match (r) + if m.end() == len(r): + if m.group(3): + version = m.group(5) + else: + version = None + return string.lower (m.group(1)), m.group(2), version + else: + return None, None, None + +class fifo: + def __init__ (self, list=None): + if not list: + self.list = [] + else: + self.list = list + + def __len__ (self): + return len(self.list) + + def first (self): + return self.list[0] + + def push_front (self, object): + self.list.insert (0, object) + + def push (self, data): + self.list.append (data) + + def pop (self): + if self.list: + result = self.list[0] + del self.list[0] + return (1, result) + else: + return (0, None) + +def compute_timezone_for_log (): + if time.daylight: + tz = time.altzone + else: + tz = time.timezone + if tz > 0: + neg = 1 + else: + neg = 0 + tz = -tz + h, rem = divmod (tz, 3600) + m, rem = divmod (rem, 60) + if neg: + return '-%02d%02d' % (h, m) + else: + return '+%02d%02d' % (h, m) + +# if you run this program over a TZ change boundary, this will be invalid. +tz_for_log = compute_timezone_for_log() + +if __name__ == '__main__': + import sys + if len(sys.argv) < 2: + print 'usage: %s <root> <port>' % (sys.argv[0]) + else: + import monitor + import filesys + import default_handler + import status_handler + import ftp_server + import chat_server + import resolver + import logger + rs = resolver.caching_resolver ('127.0.0.1') + lg = logger.file_logger (sys.stdout) + ms = monitor.secure_monitor_server ('fnord', '127.0.0.1', 9999) + fs = filesys.os_filesystem (sys.argv[1]) + dh = default_handler.default_handler (fs) + hs = http_server ('', string.atoi (sys.argv[2]), rs, lg) + hs.install_handler (dh) + ftp = ftp_server.ftp_server ( + ftp_server.dummy_authorizer(sys.argv[1]), + port=8021, + resolver=rs, + logger_object=lg + ) + cs = chat_server.chat_server ('', 7777) + sh = status_handler.status_extension([hs,ms,ftp,cs,rs]) + hs.install_handler (sh) + if ('-p' in sys.argv): + def profile_loop (): + try: + asyncore.loop() + except KeyboardInterrupt: + pass + import profile + profile.run ('profile_loop()', 'profile.out') + else: + asyncore.loop() diff --git a/demo/medusa/https_server.py b/demo/medusa/https_server.py new file mode 100644 index 0000000..1492586 --- /dev/null +++ b/demo/medusa/https_server.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python + +"""A https server built on Medusa's http_server. + +Copyright (c) 1999-2003 Ng Pheng Siong. All rights reserved.""" + +import asynchat, asyncore, http_server, socket, sys +from M2Crypto import SSL + +VERSION_STRING='0.09' + +class https_channel(http_server.http_channel): + + def __init__(self, server, conn, addr): + http_server.http_channel.__init__(self, server, conn, addr) + + def send(self, data): + try: + result = self.socket._write_nbio(data) + if result <= 0: + return 0 + else: + self.server.bytes_out.increment(result) + return result + except SSL.SSLError, why: + self.close() + self.log_info('send: closing channel %s %s' % (repr(self), why)) + return 0 + + def recv(self, buffer_size): + try: + result = self.socket._read_nbio(buffer_size) + if result is None: + return '' + elif result == '': + self.close() + return '' + else: + self.server.bytes_in.increment(len(result)) + return result + except SSL.SSLError, why: + self.close() + self.log_info('recv: closing channel %s %s' % (repr(self), why)) + return '' + + +class https_server(http_server.http_server): + + SERVER_IDENT='M2Crypto HTTPS Server (v%s)' % VERSION_STRING + + channel_class=https_channel + + def __init__(self, ip, port, ssl_ctx, resolver=None, logger_object=None): + http_server.http_server.__init__(self, ip, port, resolver, logger_object) + sys.stdout.write(self.SERVER_IDENT + '\n\n') + sys.stdout.flush() + self.ssl_ctx=ssl_ctx + + def handle_accept(self): + # Cribbed from http_server. + self.total_clients.increment() + try: + conn, addr = self.accept() + except socket.error: + # linux: on rare occasions we get a bogus socket back from + # accept. socketmodule.c:makesockaddr complains that the + # address family is unknown. We don't want the whole server + # to shut down because of this. + sys.stderr.write ('warning: server accept() threw an exception\n') + return + + # Turn the vanilla socket into an SSL connection. + try: + ssl_conn=SSL.Connection(self.ssl_ctx, conn) + ssl_conn._setup_ssl(addr) + ssl_conn.accept_ssl() + self.channel_class(self, ssl_conn, addr) + except SSL.SSLError: + pass + + def writeable(self): + return 0 + diff --git a/demo/medusa/index.html b/demo/medusa/index.html new file mode 100644 index 0000000..0f7de19 --- /dev/null +++ b/demo/medusa/index.html @@ -0,0 +1,6 @@ +<html> +<title>M2Crypto HTTPS Server</title> +<body> +<h1>M2Crypto HTTPS Server - It works!</h1> +</body> +</html> diff --git a/demo/medusa/logger.py b/demo/medusa/logger.py new file mode 100644 index 0000000..48be665 --- /dev/null +++ b/demo/medusa/logger.py @@ -0,0 +1,262 @@ +# -*- Mode: Python; tab-width: 4 -*- + +import asynchat +import socket +import string +import time # these three are for the rotating logger +import os # | +import stat # v + +# +# three types of log: +# 1) file +# with optional flushing. Also, one that rotates the log. +# 2) socket +# dump output directly to a socket connection. [how do we +# keep it open?] +# 3) syslog +# log to syslog via tcp. this is a per-line protocol. +# + +# +# The 'standard' interface to a logging object is simply +# log_object.log (message) +# + +# a file-like object that captures output, and +# makes sure to flush it always... this could +# be connected to: +# o stdio file +# o low-level file +# o socket channel +# o syslog output... + +class file_logger: + + # pass this either a path or a file object. + def __init__ (self, file, flush=1, mode='a'): + if type(file) == type(''): + if (file == '-'): + import sys + self.file = sys.stdout + else: + self.file = open (file, mode) + else: + self.file = file + self.do_flush = flush + + def __repr__ (self): + return '<file logger: %s>' % self.file + + def write (self, data): + self.file.write (data) + self.maybe_flush() + + def writeline (self, line): + self.file.writeline (line) + self.maybe_flush() + + def writelines (self, lines): + self.file.writelines (lines) + self.maybe_flush() + + def maybe_flush (self): + if self.do_flush: + self.file.flush() + + def flush (self): + self.file.flush() + + def softspace (self, *args): + pass + + def log (self, message): + if message[-1] not in ('\r', '\n'): + self.write (message + '\n') + else: + self.write (message) + +# like a file_logger, but it must be attached to a filename. +# When the log gets too full, or a certain time has passed, +# it backs up the log and starts a new one. Note that backing +# up the log is done via "mv" because anything else (cp, gzip) +# would take time, during which medusa would do nothing else. + +class rotating_file_logger (file_logger): + + # If freq is non-None we back up "daily", "weekly", or "monthly". + # Else if maxsize is non-None we back up whenever the log gets + # to big. If both are None we never back up. + def __init__ (self, file, freq=None, maxsize=None, flush=1, mode='a'): + self.filename = file + self.mode = mode + self.file = open (file, mode) + self.freq = freq + self.maxsize = maxsize + self.rotate_when = self.next_backup(self.freq) + self.do_flush = flush + + def __repr__ (self): + return '<rotating-file logger: %s>' % self.file + + # We back up at midnight every 1) day, 2) monday, or 3) 1st of month + def next_backup (self, freq): + (yr, mo, day, hr, min, sec, wd, jday, dst) = time.localtime(time.time()) + if freq == 'daily': + return time.mktime(yr,mo,day+1, 0,0,0, 0,0,-1) + elif freq == 'weekly': + return time.mktime(yr,mo,day-wd+7, 0,0,0, 0,0,-1) # wd(monday)==0 + elif freq == 'monthly': + return time.mktime(yr,mo+1,1, 0,0,0, 0,0,-1) + else: + return None # not a date-based backup + + def maybe_flush (self): # rotate first if necessary + self.maybe_rotate() + if self.do_flush: # from file_logger() + self.file.flush() + + def maybe_rotate (self): + if self.freq and time.time() > self.rotate_when: + self.rotate() + self.rotate_when = self.next_backup(self.freq) + elif self.maxsize: # rotate when we get too big + try: + if os.stat(self.filename)[stat.ST_SIZE] > self.maxsize: + self.rotate() + except os.error: # file not found, probably + self.rotate() # will create a new file + + def rotate (self): + (yr, mo, day, hr, min, sec, wd, jday, dst) = time.localtime(time.time()) + try: + self.file.close() + newname = '%s.ends%04d%02d%02d' % (self.filename, yr, mo, day) + try: + open(newname, "r").close() # check if file exists + newname = newname + "-%02d%02d%02d" % (hr, min, sec) + except: # YEARMODY is unique + pass + os.rename(self.filename, newname) + self.file = open(self.filename, self.mode) + except: + pass + +# syslog is a line-oriented log protocol - this class would be +# appropriate for FTP or HTTP logs, but not for dumping stderr to. + +# TODO: a simple safety wrapper that will ensure that the line sent +# to syslog is reasonable. + +# TODO: async version of syslog_client: now, log entries use blocking +# send() + +import m_syslog +syslog_logger = m_syslog.syslog_client + +class syslog_logger (m_syslog.syslog_client): + def __init__ (self, address, facility='user'): + m_syslog.syslog_client.__init__ (self, address) + self.facility = m_syslog.facility_names[facility] + self.address=address + + def __repr__ (self): + return '<syslog logger address=%s>' % (repr(self.address)) + + def log (self, message): + m_syslog.syslog_client.log ( + self, + message, + facility=self.facility, + priority=m_syslog.LOG_INFO + ) + +# log to a stream socket, asynchronously + +class socket_logger (asynchat.async_chat): + + def __init__ (self, address): + + if type(address) == type(''): + self.create_socket (socket.AF_UNIX, socket.SOCK_STREAM) + else: + self.create_socket (socket.AF_INET, socket.SOCK_STREAM) + + self.connect (address) + self.address = address + + def __repr__ (self): + return '<socket logger: address=%s>' % (self.address) + + def log (self, message): + if message[-2:] != '\r\n': + self.socket.push (message + '\r\n') + else: + self.socket.push (message) + +# log to multiple places +class multi_logger: + def __init__ (self, loggers): + self.loggers = loggers + + def __repr__ (self): + return '<multi logger: %s>' % (repr(self.loggers)) + + def log (self, message): + for logger in self.loggers: + logger.log (message) + +class resolving_logger: + """Feed (ip, message) combinations into this logger to get a + resolved hostname in front of the message. The message will not + be logged until the PTR request finishes (or fails).""" + + def __init__ (self, resolver, logger): + self.resolver = resolver + self.logger = logger + + class logger_thunk: + def __init__ (self, message, logger): + self.message = message + self.logger = logger + + def __call__ (self, host, ttl, answer): + if not answer: + answer = host + self.logger.log ('%s:%s' % (answer, self.message)) + + def log (self, ip, message): + self.resolver.resolve_ptr ( + ip, + self.logger_thunk ( + message, + self.logger + ) + ) + +class unresolving_logger: + "Just in case you don't want to resolve" + def __init__ (self, logger): + self.logger = logger + + def log (self, ip, message): + self.logger.log ('%s:%s' % (ip, message)) + + +def strip_eol (line): + while line and line[-1] in '\r\n': + line = line[:-1] + return line + +class tail_logger: + "Keep track of the last <size> log messages" + def __init__ (self, logger, size=500): + self.size = size + self.logger = logger + self.messages = [] + + def log (self, message): + self.messages.append (strip_eol (message)) + if len (self.messages) > self.size: + del self.messages[0] + self.logger.log (message) diff --git a/demo/medusa/m_syslog.py b/demo/medusa/m_syslog.py new file mode 100644 index 0000000..bb376db --- /dev/null +++ b/demo/medusa/m_syslog.py @@ -0,0 +1,177 @@ +# -*- Mode: Python; tab-width: 4 -*- + +# ====================================================================== +# Copyright 1997 by Sam Rushing +# +# All Rights Reserved +# +# Permission to use, copy, modify, and distribute this software and +# its documentation for any purpose and without fee is hereby +# granted, provided that the above copyright notice appear in all +# copies and that both that copyright notice and this permission +# notice appear in supporting documentation, and that the name of Sam +# Rushing not be used in advertising or publicity pertaining to +# distribution of the software without specific, written prior +# permission. +# +# SAM RUSHING DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, +# INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN +# NO EVENT SHALL SAM RUSHING BE LIABLE FOR ANY SPECIAL, INDIRECT OR +# CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +# OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, +# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +# ====================================================================== + +"""socket interface to unix syslog. +On Unix, there are usually two ways of getting to syslog: via a +local unix-domain socket, or via the TCP service. + +Usually "/dev/log" is the unix domain socket. This may be different +for other systems. + +>>> my_client = syslog_client ('/dev/log') + +Otherwise, just use the UDP version, port 514. + +>>> my_client = syslog_client (('my_log_host', 514)) + +On win32, you will have to use the UDP version. Note that +you can use this to log to other hosts (and indeed, multiple +hosts). + +This module is not a drop-in replacement for the python +<syslog> extension module - the interface is different. + +Usage: + +>>> c = syslog_client() +>>> c = syslog_client ('/strange/non_standard_log_location') +>>> c = syslog_client (('other_host.com', 514)) +>>> c.log ('testing', facility='local0', priority='debug') + +""" + +# TODO: support named-pipe syslog. +# [see ftp://sunsite.unc.edu/pub/Linux/system/Daemons/syslog-fifo.tar.z] + +# from <linux/sys/syslog.h>: +# =========================================================================== +# priorities/facilities are encoded into a single 32-bit quantity, where the +# bottom 3 bits are the priority (0-7) and the top 28 bits are the facility +# (0-big number). Both the priorities and the facilities map roughly +# one-to-one to strings in the syslogd(8) source code. This mapping is +# included in this file. +# +# priorities (these are ordered) + +LOG_EMERG = 0 # system is unusable +LOG_ALERT = 1 # action must be taken immediately +LOG_CRIT = 2 # critical conditions +LOG_ERR = 3 # error conditions +LOG_WARNING = 4 # warning conditions +LOG_NOTICE = 5 # normal but significant condition +LOG_INFO = 6 # informational +LOG_DEBUG = 7 # debug-level messages + +# facility codes +LOG_KERN = 0 # kernel messages +LOG_USER = 1 # random user-level messages +LOG_MAIL = 2 # mail system +LOG_DAEMON = 3 # system daemons +LOG_AUTH = 4 # security/authorization messages +LOG_SYSLOG = 5 # messages generated internally by syslogd +LOG_LPR = 6 # line printer subsystem +LOG_NEWS = 7 # network news subsystem +LOG_UUCP = 8 # UUCP subsystem +LOG_CRON = 9 # clock daemon +LOG_AUTHPRIV = 10 # security/authorization messages (private) + +# other codes through 15 reserved for system use +LOG_LOCAL0 = 16 # reserved for local use +LOG_LOCAL1 = 17 # reserved for local use +LOG_LOCAL2 = 18 # reserved for local use +LOG_LOCAL3 = 19 # reserved for local use +LOG_LOCAL4 = 20 # reserved for local use +LOG_LOCAL5 = 21 # reserved for local use +LOG_LOCAL6 = 22 # reserved for local use +LOG_LOCAL7 = 23 # reserved for local use + +priority_names = { + "alert": LOG_ALERT, + "crit": LOG_CRIT, + "debug": LOG_DEBUG, + "emerg": LOG_EMERG, + "err": LOG_ERR, + "error": LOG_ERR, # DEPRECATED + "info": LOG_INFO, + "notice": LOG_NOTICE, + "panic": LOG_EMERG, # DEPRECATED + "warn": LOG_WARNING, # DEPRECATED + "warning": LOG_WARNING, + } + +facility_names = { + "auth": LOG_AUTH, + "authpriv": LOG_AUTHPRIV, + "cron": LOG_CRON, + "daemon": LOG_DAEMON, + "kern": LOG_KERN, + "lpr": LOG_LPR, + "mail": LOG_MAIL, + "news": LOG_NEWS, + "security": LOG_AUTH, # DEPRECATED + "syslog": LOG_SYSLOG, + "user": LOG_USER, + "uucp": LOG_UUCP, + "local0": LOG_LOCAL0, + "local1": LOG_LOCAL1, + "local2": LOG_LOCAL2, + "local3": LOG_LOCAL3, + "local4": LOG_LOCAL4, + "local5": LOG_LOCAL5, + "local6": LOG_LOCAL6, + "local7": LOG_LOCAL7, + } + +import socket + +class syslog_client: + def __init__ (self, address='/dev/log'): + self.address = address + if type (address) == type(''): + self.socket = socket.socket (socket.AF_UNIX, socket.SOCK_STREAM) + self.socket.connect (address) + self.unix = 1 + else: + self.socket = socket.socket (socket.AF_INET, socket.SOCK_DGRAM) + self.unix = 0 + + # curious: when talking to the unix-domain '/dev/log' socket, a + # zero-terminator seems to be required. this string is placed + # into a class variable so that it can be overridden if + # necessary. + + log_format_string = '<%d>%s\000' + + def log (self, message, facility=LOG_USER, priority=LOG_INFO): + message = self.log_format_string % ( + self.encode_priority (facility, priority), + message + ) + if self.unix: + self.socket.send (message) + else: + self.socket.sendto (message, self.address) + + def encode_priority (self, facility, priority): + if type(facility) == type(''): + facility = facility_names[facility] + if type(priority) == type(''): + priority = priority_names[priority] + return (facility<<3) | priority + + def close (self): + if self.unix: + self.socket.close() + diff --git a/demo/medusa/medusa_gif.py b/demo/medusa/medusa_gif.py new file mode 100644 index 0000000..005644a --- /dev/null +++ b/demo/medusa/medusa_gif.py @@ -0,0 +1,8 @@ +# -*- Mode: Python -*- + +# the medusa icon as a python source file. + +width = 97 +height = 61 + +data = 'GIF89aa\000=\000\204\000\000\000\000\000\255\255\255\245\245\245ssskkkccc111)))\326\326\326!!!\316\316\316\300\300\300\204\204\000\224\224\224\214\214\214\200\200\200RRR\377\377\377JJJ\367\367\367BBB\347\347\347\000\204\000\020\020\020\265\265\265\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000!\371\004\001\000\000\021\000,\000\000\000\000a\000=\000\000\005\376`$\216di\236h\252\256l\353\276p,\317tm\337x\256\357|m\001@\240E\305\000\364\2164\206R)$\005\201\214\007r\012{X\255\312a\004\260\\>\026\3240\353)\224n\001W+X\334\373\231~\344.\303b\216\024\027x<\273\307\255G,rJiWN\014{S}k"?ti\013EdPQ\207G@_%\000\026yy\\\201\202\227\224<\221Fs$pOjWz\241<r@vO\236\231\233k\247M\2544\203F\177\235\236L#\247\256Z\270,\266BxJ[\276\256A]iE\304\305\262\273E\313\201\275i#\\\303\321\'h\203V\\\177\326\276\216\220P~\335\230_\264\013\342\275\344KF\233\360Q\212\352\246\000\367\274s\361\236\334\347T\341;\341\246\2202\177\3142\211`\242o\325@S\202\264\031\252\207\260\323\256\205\311\036\236\270\002\'\013\302\177\274H\010\324X\002\0176\212\037\376\321\360\032\226\207\244\2674(+^\202\346r\205J\0211\375\241Y#\256f\0127\315>\272\002\325\307g\012(\007\205\312#j\317(\012A\200\224.\241\003\346GS\247\033\245\344\264\366\015L\'PXQl]\266\263\243\232\260?\245\316\371\362\225\035\332\243J\273\332Q\263\357-D\241T\327\270\265\013W&\330\010u\371b\322IW0\214\261]\003\033Va\365Z#\207\213a\030k\2647\262\014p\354\024[n\321N\363\346\317\003\037P\000\235C\302\000\3228(\244\363YaA\005\022\255_\237@\260\000A\212\326\256qbp\321\332\266\011\334=T\023\010"!B\005\003A\010\224\020\220 H\002\337#\020 O\276E\357h\221\327\003\\\000b@v\004\351A.h\365\354\342B\002\011\257\025\\ \220\340\301\353\006\000\024\214\200pA\300\353\012\364\241k/\340\033C\202\003\000\310fZ\011\003V\240R\005\007\354\376\026A\000\000\360\'\202\177\024\004\210\003\000\305\215\360\000\000\015\220\240\332\203\027@\'\202\004\025VpA\000%\210x\321\206\032J\341\316\010\262\211H"l\333\341\200\200>"]P\002\212\011\010`\002\0066FP\200\001\'\024p]\004\027(8B\221\306]\000\201w>\002iB\001\007\340\260"v7J1\343(\257\020\251\243\011\242i\263\017\215\337\035\220\200\221\365m4d\015\016D\251\341iN\354\346Ng\253\200I\240\031\35609\245\2057\311I\302\2007t\231"&`\314\310\244\011e\226(\236\010w\212\300\234\011\012HX(\214\253\311@\001\233^\222pg{% \340\035\224&H\000\246\201\362\215`@\001"L\340\004\030\234\022\250\'\015(V:\302\235\030\240q\337\205\224\212h@\177\006\000\250\210\004\007\310\207\337\005\257-P\346\257\367]p\353\203\271\256:\203\236\211F\340\247\010\3329g\244\010\307*=A\000\203\260y\012\304s#\014\007D\207,N\007\304\265\027\021C\233\207%B\366[m\353\006\006\034j\360\306+\357\274a\204\000\000;' diff --git a/demo/medusa/mime_type_table.py b/demo/medusa/mime_type_table.py new file mode 100644 index 0000000..dbc64a4 --- /dev/null +++ b/demo/medusa/mime_type_table.py @@ -0,0 +1,113 @@ +# -*- Python -*- +# Converted by ./convert_mime_type_table.py from: +# /usr/src2/apache_1.2b6/conf/mime.types +# +content_type_map = \ + { + 'ai': 'application/postscript', + 'aif': 'audio/x-aiff', + 'aifc': 'audio/x-aiff', + 'aiff': 'audio/x-aiff', + 'au': 'audio/basic', + 'avi': 'video/x-msvideo', + 'bcpio': 'application/x-bcpio', + 'bin': 'application/octet-stream', + 'cdf': 'application/x-netcdf', + 'class': 'application/octet-stream', + 'cpio': 'application/x-cpio', + 'cpt': 'application/mac-compactpro', + 'csh': 'application/x-csh', + 'dcr': 'application/x-director', + 'dir': 'application/x-director', + 'dms': 'application/octet-stream', + 'doc': 'application/msword', + 'dvi': 'application/x-dvi', + 'dxr': 'application/x-director', + 'eps': 'application/postscript', + 'etx': 'text/x-setext', + 'exe': 'application/octet-stream', + 'gif': 'image/gif', + 'gtar': 'application/x-gtar', + 'gz': 'application/x-gzip', + 'hdf': 'application/x-hdf', + 'hqx': 'application/mac-binhex40', + 'htm': 'text/html', + 'html': 'text/html', + 'ice': 'x-conference/x-cooltalk', + 'ief': 'image/ief', + 'jpe': 'image/jpeg', + 'jpeg': 'image/jpeg', + 'jpg': 'image/jpeg', + 'kar': 'audio/midi', + 'latex': 'application/x-latex', + 'lha': 'application/octet-stream', + 'lzh': 'application/octet-stream', + 'man': 'application/x-troff-man', + 'me': 'application/x-troff-me', + 'mid': 'audio/midi', + 'midi': 'audio/midi', + 'mif': 'application/x-mif', + 'mov': 'video/quicktime', + 'movie': 'video/x-sgi-movie', + 'mp2': 'audio/mpeg', + 'mpe': 'video/mpeg', + 'mpeg': 'video/mpeg', + 'mpg': 'video/mpeg', + 'mpga': 'audio/mpeg', + 'mp3': 'audio/mpeg', + 'ms': 'application/x-troff-ms', + 'nc': 'application/x-netcdf', + 'oda': 'application/oda', + 'pbm': 'image/x-portable-bitmap', + 'pdb': 'chemical/x-pdb', + 'pdf': 'application/pdf', + 'pgm': 'image/x-portable-graymap', + 'png': 'image/png', + 'pnm': 'image/x-portable-anymap', + 'ppm': 'image/x-portable-pixmap', + 'ppt': 'application/powerpoint', + 'ps': 'application/postscript', + 'qt': 'video/quicktime', + 'ra': 'audio/x-realaudio', + 'ram': 'audio/x-pn-realaudio', + 'ras': 'image/x-cmu-raster', + 'rgb': 'image/x-rgb', + 'roff': 'application/x-troff', + 'rpm': 'audio/x-pn-realaudio-plugin', + 'rtf': 'application/rtf', + 'rtx': 'text/richtext', + 'sgm': 'text/x-sgml', + 'sgml': 'text/x-sgml', + 'sh': 'application/x-sh', + 'shar': 'application/x-shar', + 'sit': 'application/x-stuffit', + 'skd': 'application/x-koan', + 'skm': 'application/x-koan', + 'skp': 'application/x-koan', + 'skt': 'application/x-koan', + 'snd': 'audio/basic', + 'src': 'application/x-wais-source', + 'sv4cpio': 'application/x-sv4cpio', + 'sv4crc': 'application/x-sv4crc', + 't': 'application/x-troff', + 'tar': 'application/x-tar', + 'tcl': 'application/x-tcl', + 'tex': 'application/x-tex', + 'texi': 'application/x-texinfo', + 'texinfo': 'application/x-texinfo', + 'tif': 'image/tiff', + 'tiff': 'image/tiff', + 'tr': 'application/x-troff', + 'tsv': 'text/tab-separated-values', + 'txt': 'text/plain', + 'ustar': 'application/x-ustar', + 'vcd': 'application/x-cdlink', + 'vrml': 'x-world/x-vrml', + 'wav': 'audio/x-wav', + 'wrl': 'x-world/x-vrml', + 'xbm': 'image/x-xbitmap', + 'xpm': 'image/x-xpixmap', + 'xwd': 'image/x-xwindowdump', + 'xyz': 'chemical/x-pdb', + 'zip': 'application/zip', + } diff --git a/demo/medusa/poison_handler.py b/demo/medusa/poison_handler.py new file mode 100644 index 0000000..acc78ab --- /dev/null +++ b/demo/medusa/poison_handler.py @@ -0,0 +1,69 @@ + +import string +import whrandom + +RESP_HEAD="""\ +<HTML><BODY BGCOLOR=\"#ffffff\"> +""" + +RESP_MIDDLE=""" +<h2>M2Crypto https server demonstration</h2> + +This web page is generated by the "poison" http request handler. +<br> +The links just go on and on and on... +<br><br> +""" + +RESP_TAIL=""" +</BODY></HTML> +""" + +charset='012345678/90ABCDEFGHIJKLM/NOPQRSTUVWXYZabcd/efghijklmnopqrs/tuvwxyz' +numchar=len(charset) + +def makepage(numlinks): + + title='<title>' + for u in range(whrandom.randint(3, 15)): + pick=whrandom.randint(0, numchar-1) + title=title+charset[pick] + title=title+'</title>' + + url='\r\n' + numlinks=whrandom.randint(2, numlinks) + for i in range(numlinks): + url=url+'<a href="/poison/' + for u in range(whrandom.randint(3, 15)): + pick=whrandom.randint(0, numchar-1) + ch=charset[pick] + if ch=='/' and url[-1]=='/': + ch=charset[pick+1] + url=url+ch + url=url+'/">' + for u in range(whrandom.randint(3, 15)): + pick=whrandom.randint(0, numchar-1) + url=url+charset[pick] + url=url+'</a><br>\r\n' + + url=RESP_HEAD+title+RESP_MIDDLE+url+RESP_TAIL + return url + + +class poison_handler: + """This is a clone of webpoison - every URL returns a page of URLs, each of which + returns a page of URLs, each of _which_ returns a page of URLs, ad infinitum. + The objective is to sucker address-harvesting bots run by spammers.""" + + def __init__(self, numlinks=10): + self.numlinks = numlinks + self.poison_level = 0 + + def match(self, request): + return (request.uri[:7] == '/poison') + + def handle_request(self, request): + if request.command == 'get': + request.push(makepage(self.numlinks)) + request.done() + diff --git a/demo/medusa/producers.py b/demo/medusa/producers.py new file mode 100644 index 0000000..2138258 --- /dev/null +++ b/demo/medusa/producers.py @@ -0,0 +1,329 @@ +# -*- Mode: Python; tab-width: 4 -*- + +import string + +""" +A collection of producers. +Each producer implements a particular feature: They can be combined +in various ways to get interesting and useful behaviors. + +For example, you can feed dynamically-produced output into the compressing +producer, then wrap this with the 'chunked' transfer-encoding producer. +""" + +class simple_producer: + "producer for a string" + def __init__ (self, data, buffer_size=1024): + self.data = data + self.buffer_size = buffer_size + + def more (self): + if len (self.data) > self.buffer_size: + result = self.data[:self.buffer_size] + self.data = self.data[self.buffer_size:] + return result + else: + result = self.data + self.data = '' + return result + +class scanning_producer: + "like simple_producer, but more efficient for large strings" + def __init__ (self, data, buffer_size=1024): + self.data = data + self.buffer_size = buffer_size + self.pos = 0 + + def more (self): + if self.pos < len(self.data): + lp = self.pos + rp = min ( + len(self.data), + self.pos + self.buffer_size + ) + result = self.data[lp:rp] + self.pos = self.pos + len(result) + return result + else: + return '' + +class lines_producer: + "producer for a list of lines" + + def __init__ (self, lines): + self.lines = lines + + def ready (self): + return len(self.lines) + + def more (self): + if self.lines: + chunk = self.lines[:50] + self.lines = self.lines[50:] + return string.join (chunk, '\r\n') + '\r\n' + else: + return '' + +class buffer_list_producer: + "producer for a list of buffers" + + # i.e., data == string.join (buffers, '') + + def __init__ (self, buffers): + + self.index = 0 + self.buffers = buffers + + def more (self): + if self.index >= len(self.buffers): + return '' + else: + data = self.buffers[self.index] + self.index = self.index + 1 + return data + +class file_producer: + "producer wrapper for file[-like] objects" + + # match http_channel's outgoing buffer size + out_buffer_size = 1<<16 + + def __init__ (self, file): + self.done = 0 + self.file = file + + def more (self): + if self.done: + return '' + else: + data = self.file.read (self.out_buffer_size) + if not data: + self.file.close() + del self.file + self.done = 1 + return '' + else: + return data + +# A simple output producer. This one does not [yet] have +# the safety feature builtin to the monitor channel: runaway +# output will not be caught. + +# don't try to print from within any of the methods +# of this object. + +class output_producer: + "Acts like an output file; suitable for capturing sys.stdout" + def __init__ (self): + self.data = '' + + def write (self, data): + lines = string.splitfields (data, '\n') + data = string.join (lines, '\r\n') + self.data = self.data + data + + def writeline (self, line): + self.data = self.data + line + '\r\n' + + def writelines (self, lines): + self.data = self.data + string.joinfields ( + lines, + '\r\n' + ) + '\r\n' + + def ready (self): + return (len (self.data) > 0) + + def flush (self): + pass + + def softspace (self, *args): + pass + + def more (self): + if self.data: + result = self.data[:512] + self.data = self.data[512:] + return result + else: + return '' + +class composite_producer: + "combine a fifo of producers into one" + def __init__ (self, producers): + self.producers = producers + + def more (self): + while len(self.producers): + p = self.producers.first() + d = p.more() + if d: + return d + else: + self.producers.pop() + else: + return '' + + +class globbing_producer: + """ + 'glob' the output from a producer into a particular buffer size. + helps reduce the number of calls to send(). [this appears to + gain about 30% performance on requests to a single channel] + """ + + def __init__ (self, producer, buffer_size=1<<16): + self.producer = producer + self.buffer = '' + self.buffer_size = buffer_size + + def more (self): + while len(self.buffer) < self.buffer_size: + data = self.producer.more() + if data: + self.buffer = self.buffer + data + else: + break + r = self.buffer + self.buffer = '' + return r + + +class hooked_producer: + """ + A producer that will call <function> when it empties,. + with an argument of the number of bytes produced. Useful + for logging/instrumentation purposes. + """ + + def __init__ (self, producer, function): + self.producer = producer + self.function = function + self.bytes = 0 + + def more (self): + if self.producer: + result = self.producer.more() + if not result: + self.producer = None + self.function (self.bytes) + else: + self.bytes = self.bytes + len(result) + return result + else: + return '' + +# HTTP 1.1 emphasizes that an advertised Content-Length header MUST be +# correct. In the face of Strange Files, it is conceivable that +# reading a 'file' may produce an amount of data not matching that +# reported by os.stat() [text/binary mode issues, perhaps the file is +# being appended to, etc..] This makes the chunked encoding a True +# Blessing, and it really ought to be used even with normal files. +# How beautifully it blends with the concept of the producer. + +class chunked_producer: + """A producer that implements the 'chunked' transfer coding for HTTP/1.1. + Here is a sample usage: + request['Transfer-Encoding'] = 'chunked' + request.push ( + producers.chunked_producer (your_producer) + ) + request.done() + """ + + def __init__ (self, producer, footers=None): + self.producer = producer + self.footers = footers + + def more (self): + if self.producer: + data = self.producer.more() + if data: + return '%x\r\n%s\r\n' % (len(data), data) + else: + self.producer = None + if self.footers: + return string.join ( + ['0'] + self.footers, + '\r\n' + ) + '\r\n\r\n' + else: + return '0\r\n\r\n' + else: + return '' + +# Unfortunately this isn't very useful right now (Aug 97), because +# apparently the browsers don't do on-the-fly decompression. Which +# is sad, because this could _really_ speed things up, especially for +# low-bandwidth clients (i.e., most everyone). + +try: + import zlib +except ImportError: + zlib = None + +class compressed_producer: + """ + Compress another producer on-the-fly, using ZLIB + [Unfortunately, none of the current browsers seem to support this] + """ + + # Note: It's not very efficient to have the server repeatedly + # compressing your outgoing files: compress them ahead of time, or + # use a compress-once-and-store scheme. However, if you have low + # bandwidth and low traffic, this may make more sense than + # maintaining your source files compressed. + # + # Can also be used for compressing dynamically-produced output. + + def __init__ (self, producer, level=5): + self.producer = producer + self.compressor = zlib.compressobj (level) + + def more (self): + if self.producer: + cdata = '' + # feed until we get some output + while not cdata: + data = self.producer.more() + if not data: + self.producer = None + return self.compressor.flush() + else: + cdata = self.compressor.compress (data) + return cdata + else: + return '' + +class escaping_producer: + + "A producer that escapes a sequence of characters" + " Common usage: escaping the CRLF.CRLF sequence in SMTP, NNTP, etc..." + + def __init__ (self, producer, esc_from='\r\n.', esc_to='\r\n..'): + self.producer = producer + self.esc_from = esc_from + self.esc_to = esc_to + self.buffer = '' + from asynchat import find_prefix_at_end + self.find_prefix_at_end = find_prefix_at_end + + def more (self): + esc_from = self.esc_from + esc_to = self.esc_to + + buffer = self.buffer + self.producer.more() + + if buffer: + buffer = string.replace (buffer, esc_from, esc_to) + i = self.find_prefix_at_end (buffer, esc_from) + if i: + # we found a prefix + self.buffer = buffer[-i:] + return buffer[:-i] + else: + # no prefix, return it all + self.buffer = '' + return buffer + else: + return buffer diff --git a/demo/medusa/put_handler.py b/demo/medusa/put_handler.py new file mode 100644 index 0000000..488fb42 --- /dev/null +++ b/demo/medusa/put_handler.py @@ -0,0 +1,113 @@ +# -*- Mode: Python; tab-width: 4 -*- +# +# Author: Sam Rushing <rushing@nightmare.com> +# Copyright 1996-2000 by Sam Rushing +# All Rights Reserved. +# + +import re +import string + +import default_handler +unquote = default_handler.unquote +get_header = default_handler.get_header + +last_request = None + +class put_handler: + def __init__ (self, filesystem, uri_regex): + self.filesystem = filesystem + if type (uri_regex) == type(''): + self.uri_regex = re.compile (uri_regex) + else: + self.uri_regex = uri_regex + + def match (self, request): + uri = request.uri + if request.command == 'put': + m = self.uri_regex.match (uri) + if m and m.end() == len(uri): + return 1 + return 0 + + def handle_request (self, request): + + path, params, query, fragment = request.split_uri() + + # strip off leading slashes + while path and path[0] == '/': + path = path[1:] + + if '%' in path: + path = unquote (path) + + # make sure there's a content-length header + cl = get_header (CONTENT_LENGTH, request.header) + if not cl: + request.error (411) + return + else: + cl = string.atoi (cl) + + # don't let the try to overwrite a directory + if self.filesystem.isdir (path): + request.error (405) + return + + is_update = self.filesystem.isfile (path) + + try: + output_file = self.filesystem.open (path, 'wb') + except: + request.error (405) + return + + request.collector = put_collector (output_file, cl, request, is_update) + + # no terminator while receiving PUT data + request.channel.set_terminator (None) + + # don't respond yet, wait until we've received the data... + +class put_collector: + def __init__ (self, file, length, request, is_update): + self.file = file + self.length = length + self.request = request + self.is_update = is_update + self.bytes_in = 0 + + def collect_incoming_data (self, data): + ld = len(data) + bi = self.bytes_in + if (bi + ld) >= self.length: + # last bit of data + chunk = self.length - bi + self.file.write (data[:chunk]) + self.file.close() + + if chunk != ld: + print 'orphaned %d bytes: <%s>' % (ld - chunk, repr(data[chunk:])) + + # do some housekeeping + r = self.request + ch = r.channel + ch.current_request = None + # set the terminator back to the default + ch.set_terminator ('\r\n\r\n') + if self.is_update: + r.reply_code = 204 # No content + r.done() + else: + r.reply_now (201) # Created + # avoid circular reference + del self.request + else: + self.file.write (data) + self.bytes_in = self.bytes_in + ld + + def found_terminator (self): + # shouldn't be called + pass + +CONTENT_LENGTH = re.compile ('Content-Length: ([0-9]+)', re.IGNORECASE) diff --git a/demo/medusa/redirecting_handler.py b/demo/medusa/redirecting_handler.py new file mode 100644 index 0000000..e6a1e90 --- /dev/null +++ b/demo/medusa/redirecting_handler.py @@ -0,0 +1,44 @@ +# -*- Mode: Python; tab-width: 4 -*- +# +# Author: Sam Rushing <rushing@nightmare.com> +# Copyright 1996-2000 by Sam Rushing +# All Rights Reserved. +# + +import re +import counter + +class redirecting_handler: + + def __init__ (self, pattern, redirect, regex_flag=re.IGNORECASE): + self.pattern = pattern + self.redirect = redirect + self.patreg = re.compile (pattern, regex_flag) + self.hits = counter.counter() + + def match (self, request): + m = self.patref.match (request.uri) + return (m and (m.end() == len(request.uri))) + + def handle_request (self, request): + self.hits.increment() + m = self.patreg.match (request.uri) + part = m.group(1) + + request['Location'] = self.redirect % part + request.error (302) # moved temporarily + + def __repr__ (self): + return '<Redirecting Handler at %08x [%s => %s]>' % ( + id(self), + repr(self.pattern), + repr(self.redirect) + ) + + def status (self): + import producers + return producers.simple_producer ( + '<li> Redirecting Handler %s => %s <b>Hits</b>: %s' % ( + self.pattern, self.redirect, self.hits + ) + ) diff --git a/demo/medusa/server.pem b/demo/medusa/server.pem new file mode 100644 index 0000000..1ee9282 --- /dev/null +++ b/demo/medusa/server.pem @@ -0,0 +1,36 @@ +-----BEGIN CERTIFICATE----- +MIIDYjCCAsugAwIBAgIBBDANBgkqhkiG9w0BAQQFADCBgDELMAkGA1UEBhMCU0cx +ETAPBgNVBAoTCE0yQ3J5cHRvMRQwEgYDVQQLEwtNMkNyeXB0byBDQTEkMCIGA1UE +AxMbTTJDcnlwdG8gQ2VydGlmaWNhdGUgTWFzdGVyMSIwIAYJKoZIhvcNAQkBFhNu +Z3BzQG5ldG1lbWV0aWMuY29tMB4XDTAzMDYyMjEzMzAxNFoXDTA0MDYyMTEzMzAx +NFowXTELMAkGA1UEBhMCU0cxETAPBgNVBAoTCE0yQ3J5cHRvMRIwEAYDVQQDEwls +b2NhbGhvc3QxJzAlBgkqhkiG9w0BCQEWGGFkbWluQHNlcnZlci5leGFtcGxlLmRv +bTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA37aKGZtFicl8xXXTLJ8/JD7c +kd3t/teCX9i61lpaDQCKBoVrrursIvZihemMKI9g/u/+BLqt5g8mBdgUdYz0txc8 +KEbV2hj+wwOX4H3XwD0Y+DysXiNHq7/tFdmzSVHoLxpY4zYzXbxQ/p049wvIyPRp +/y3omcnx/TEUhkn+JmkCAwEAAaOCAQwwggEIMAkGA1UdEwQCMAAwLAYJYIZIAYb4 +QgENBB8WHU9wZW5TU0wgR2VuZXJhdGVkIENlcnRpZmljYXRlMB0GA1UdDgQWBBTB +H/mUYlww24mJlSxEtGdlwojO9zCBrQYDVR0jBIGlMIGigBTr+pwHMS1CmF9cuGI4 +du3YqoHAwqGBhqSBgzCBgDELMAkGA1UEBhMCU0cxETAPBgNVBAoTCE0yQ3J5cHRv +MRQwEgYDVQQLEwtNMkNyeXB0byBDQTEkMCIGA1UEAxMbTTJDcnlwdG8gQ2VydGlm +aWNhdGUgTWFzdGVyMSIwIAYJKoZIhvcNAQkBFhNuZ3BzQG5ldG1lbWV0aWMuY29t +ggEAMA0GCSqGSIb3DQEBBAUAA4GBAAvl6v0s3eFeGP4iAcrfysuK7jzFKhjDYuOy +lVS3u33bZNLnMpM6OSEM9yPh4WpFCVHf+nYwC71pk4ilsLVXjKxymm2lNGcxLVuW +iydFz4Ly9nmN7Ja9ygYT39dGAFP/wN7ELTpsbul8VfmqhNg9y81d8i/A1tK3AGA8 +0QkPQNdP +-----END CERTIFICATE----- +-----BEGIN RSA PRIVATE KEY----- +MIICXAIBAAKBgQDftooZm0WJyXzFddMsnz8kPtyR3e3+14Jf2LrWWloNAIoGhWuu +6uwi9mKF6Ywoj2D+7/4Euq3mDyYF2BR1jPS3FzwoRtXaGP7DA5fgfdfAPRj4PKxe +I0erv+0V2bNJUegvGljjNjNdvFD+nTj3C8jI9Gn/LeiZyfH9MRSGSf4maQIDAQAB +AoGAHpeVtv62uarl9xKvuBBm0AwQmZnhq9HIsFaw5hMg8Vo7hbzFBvx1IirTOkC/ +u+QvfW1QLVFh6m3z4ySzV4fZBtcd6F9SbSrZ0xsxIUB2NOSa1RGgiaP61bJnMMM1 +xM3O9iwM5GZc3Gqy08QOCpDl0772VJ+9Gz3FA88mrc6rHQkCQQDz6RIatFjT28n8 +1vy0nHwwZz2oXTpe/pyZPwoKj8zVsmrKhKwOw7l8ArxjP8zoHOE7AQBCXYDMNoFp +IAF0yuqrAkEA6s0wMEdPpQeb0XHAfccyJQoeULxHdVFoz1wWmGSOm4YmQtR8/QJx +luEgfpeRkzxBKt5Ls3MEkheIOw7xV24zOwJAMz+DaE0AZPNHS3K4ghJnHZxzng6I +lzEUIjbWm0V/ml70hTy/EhMZw+6nOotLOHHo+QbK0SboSwAgzL/Gzo1cJQJANqpS +38qadleRJXAQWrg3qnvyluVe1aeAeVZ9RDmVIgxXeBO0jcs12uTLBe4PzHGo0mwy +v7K1i7XC180gzzQu5QJBAOxITT9RoWSSozPvnirHd37sn+RsrNYkV07NAa80M20Z +DkBPHeMVkNgigrQ6L6vWmbRDGQbGcMplAxnI5ppKCoU= +-----END RSA PRIVATE KEY----- diff --git a/demo/medusa/status_handler.py b/demo/medusa/status_handler.py new file mode 100644 index 0000000..2e6223e --- /dev/null +++ b/demo/medusa/status_handler.py @@ -0,0 +1,282 @@ +# -*- Mode: Python; tab-width: 4 -*- + +VERSION_STRING = "$Id: status_handler.py 299 2005-06-09 17:32:28Z heikki $" + +# +# medusa status extension +# + +import string +import time +import re + +import asyncore +import http_server +import medusa_gif +import producers +from counter import counter + +START_TIME = long(time.time()) + +class status_extension: + hit_counter = counter() + + def __init__ (self, objects, statusdir='/status', allow_emergency_debug=0): + self.objects = objects + self.statusdir = statusdir + self.allow_emergency_debug = allow_emergency_debug + # We use /status instead of statusdir here because it's too + # hard to pass statusdir to the logger, who makes the HREF + # to the object dir. We don't need the security-through- + # obscurity here in any case, because the id is obscurity enough + self.hyper_regex = re.compile('/status/object/([0-9]+)/.*') + self.hyper_objects = [] + for object in objects: + self.register_hyper_object (object) + + def __repr__ (self): + return '<Status Extension (%s hits) at %x>' % ( + self.hit_counter, + id(self) + ) + + def match (self, request): + path, params, query, fragment = request.split_uri() + # For reasons explained above, we don't use statusdir for /object + return (path[:len(self.statusdir)] == self.statusdir or + path[:len("/status/object/")] == '/status/object/') + + # Possible Targets: + # /status + # /status/channel_list + # /status/medusa.gif + + # can we have 'clickable' objects? + # [yes, we can use id(x) and do a linear search] + + # Dynamic producers: + # HTTP/1.0: we must close the channel, because it's dynamic output + # HTTP/1.1: we can use the chunked transfer-encoding, and leave + # it open. + + def handle_request (self, request): + [path, params, query, fragment] = request.split_uri() + self.hit_counter.increment() + if path == self.statusdir: # and not a subdirectory + up_time = string.join (english_time (long(time.time()) - START_TIME)) + request['Content-Type'] = 'text/html' + request.push ( + '<html>' + '<title>Medusa Status Reports</title>' + '<body bgcolor="#ffffff">' + '<h1>Medusa Status Reports</h1>' + '<b>Up:</b> %s' % up_time + ) + for i in range(len(self.objects)): + request.push (self.objects[i].status()) + request.push ('<hr>\r\n') + request.push ( + '<p><a href="%s/channel_list">Channel List</a>' + '<hr>' + '<img src="%s/medusa.gif" align=right width=%d height=%d>' + '</body></html>' % ( + self.statusdir, + self.statusdir, + medusa_gif.width, + medusa_gif.height + ) + ) + request.done() + elif path == self.statusdir + '/channel_list': + request['Content-Type'] = 'text/html' + request.push ('<html><body>') + request.push(channel_list_producer(self.statusdir)) + request.push ( + '<hr>' + '<img src="%s/medusa.gif" align=right width=%d height=%d>' % ( + self.statusdir, + medusa_gif.width, + medusa_gif.height + ) + + '</body></html>' + ) + request.done() + + elif path == self.statusdir + '/medusa.gif': + request['Content-Type'] = 'image/gif' + request['Content-Length'] = len(medusa_gif.data) + request.push (medusa_gif.data) + request.done() + + elif path == self.statusdir + '/close_zombies': + message = ( + '<h2>Closing all zombie http client connections...</h2>' + '<p><a href="%s">Back to the status page</a>' % self.statusdir + ) + request['Content-Type'] = 'text/html' + request['Content-Length'] = len (message) + request.push (message) + now = int (time.time()) + for channel in asyncore.socket_map.keys(): + if channel.__class__ == http_server.http_channel: + if channel != request.channel: + if (now - channel.creation_time) > channel.zombie_timeout: + channel.close() + request.done() + + # Emergency Debug Mode + # If a server is running away from you, don't KILL it! + # Move all the AF_INET server ports and perform an autopsy... + # [disabled by default to protect the innocent] + elif self.allow_emergency_debug and path == self.statusdir + '/emergency_debug': + request.push ('<html>Moving All Servers...</html>') + request.done() + for channel in asyncore.socket_map.keys(): + if channel.accepting: + if type(channel.addr) is type(()): + ip, port = channel.addr + channel.socket.close() + channel.del_channel() + channel.addr = (ip, port+10000) + fam, typ = channel.family_and_type + channel.create_socket (fam, typ) + channel.set_reuse_addr() + channel.bind (channel.addr) + channel.listen(5) + + else: + m = self.hyper_regex.match (path) + if m: + oid = string.atoi (m.group (1)) + for object in self.hyper_objects: + if id (object) == oid: + if hasattr (object, 'hyper_respond'): + object.hyper_respond (self, path, request) + else: + request.error (404) + return + + def status (self): + return producers.simple_producer ( + '<li>Status Extension <b>Hits</b> : %s' % self.hit_counter + ) + + def register_hyper_object (self, object): + if not object in self.hyper_objects: + self.hyper_objects.append (object) + +import logger + +class logger_for_status (logger.tail_logger): + + def status (self): + return 'Last %d log entries for: %s' % ( + len (self.messages), + html_repr (self) + ) + + def hyper_respond (self, sh, path, request): + request['Content-Type'] = 'text/plain' + messages = self.messages[:] + messages.reverse() + request.push (lines_producer (messages)) + request.done() + +class lines_producer: + def __init__ (self, lines): + self.lines = lines + + def ready (self): + return len(self.lines) + + def more (self): + if self.lines: + chunk = self.lines[:50] + self.lines = self.lines[50:] + return string.join (chunk, '\r\n') + '\r\n' + else: + return '' + +class channel_list_producer (lines_producer): + def __init__ (self, statusdir): + channel_reprs = map ( + lambda x: '<' + repr(x)[1:-1] + '>', + asyncore.socket_map.values() + ) + channel_reprs.sort() + lines_producer.__init__ ( + self, + ['<h1>Active Channel List</h1>', + '<pre>' + ] + channel_reprs + [ + '</pre>', + '<p><a href="%s">Status Report</a>' % statusdir + ] + ) + + +# this really needs a full-blown quoter... +def sanitize (s): + if '<' in s: + s = string.join (string.split (s, '<'), '<') + if '>' in s: + s = string.join (string.split (s, '>'), '>') + return s + +def html_repr (object): + so = sanitize (repr (object)) + if hasattr (object, 'hyper_respond'): + return '<a href="/status/object/%d/">%s</a>' % (id (object), so) + else: + return so + +def html_reprs (list, front='', back=''): + reprs = map ( + lambda x,f=front,b=back: '%s%s%s' % (f,x,b), + map (lambda x: sanitize (html_repr(x)), list) + ) + reprs.sort() + return reprs + +# for example, tera, giga, mega, kilo +# p_d (n, (1024, 1024, 1024, 1024)) +# smallest divider goes first - for example +# minutes, hours, days +# p_d (n, (60, 60, 24)) + +def progressive_divide (n, parts): + result = [] + for part in parts: + n, rem = divmod (n, part) + result.append (rem) + result.append (n) + return result + +# b,k,m,g,t +def split_by_units (n, units, dividers, format_string): + divs = progressive_divide (n, dividers) + result = [] + for i in range(len(units)): + if divs[i]: + result.append (format_string % (divs[i], units[i])) + result.reverse() + if not result: + return [format_string % (0, units[0])] + else: + return result + +def english_bytes (n): + return split_by_units ( + n, + ('','K','M','G','T'), + (1024, 1024, 1024, 1024, 1024), + '%d %sB' + ) + +def english_time (n): + return split_by_units ( + n, + ('secs', 'mins', 'hours', 'days', 'weeks', 'years'), + ( 60, 60, 24, 7, 52), + '%d %s' + ) diff --git a/demo/medusa/virtual_handler.py b/demo/medusa/virtual_handler.py new file mode 100644 index 0000000..96f19df --- /dev/null +++ b/demo/medusa/virtual_handler.py @@ -0,0 +1,60 @@ +# -*- Mode: Python; tab-width: 4 -*- + +import socket +import default_handler +import re + +HOST = re.compile ('Host: ([^:/]+).*', re.IGNORECASE) + +get_header = default_handler.get_header + +class virtual_handler: + + """HTTP request handler for an HTTP/1.0-style virtual host. Each + Virtual host must have a different IP""" + + def __init__ (self, handler, hostname): + self.handler = handler + self.hostname = hostname + try: + self.ip = socket.gethostbyname (hostname) + except socket.error: + raise ValueError, "Virtual Hostname %s does not appear to be registered in the DNS" % hostname + + def match (self, request): + if (request.channel.addr[0] == self.ip): + return 1 + else: + return 0 + + def handle_request (self, request): + return self.handler.handle_request (request) + + def __repr__ (self): + return '<virtual request handler for %s>' % self.hostname + + +class virtual_handler_with_host: + + """HTTP request handler for HTTP/1.1-style virtual hosts. This + matches by checking the value of the 'Host' header in the request. + You actually don't _have_ to support HTTP/1.1 to use this, since + many browsers now send the 'Host' header. This is a Good Thing.""" + + def __init__ (self, handler, hostname): + self.handler = handler + self.hostname = hostname + + def match (self, request): + host = get_header (HOST, request.header) + if host == self.hostname: + return 1 + else: + return 0 + + def handle_request (self, request): + return self.handler.handle_request (request) + + def __repr__ (self): + return '<virtual request handler for %s>' % self.hostname + diff --git a/demo/medusa/xmlrpc_handler.py b/demo/medusa/xmlrpc_handler.py new file mode 100644 index 0000000..d56baf2 --- /dev/null +++ b/demo/medusa/xmlrpc_handler.py @@ -0,0 +1,104 @@ +# -*- Mode: Python; tab-width: 4 -*- + +# See http://www.xml-rpc.com/ +# http://www.pythonware.com/products/xmlrpc/ + +# Based on "xmlrpcserver.py" by Fredrik Lundh (fredrik@pythonware.com) + +VERSION = "$Id: xmlrpc_handler.py 299 2005-06-09 17:32:28Z heikki $" + +import http_server +import xmlrpclib + +import string +import sys + +class xmlrpc_handler: + + def match (self, request): + # Note: /RPC2 is not required by the spec, so you may override this method. + if request.uri[:5] == '/RPC2': + return 1 + else: + return 0 + + def handle_request (self, request): + [path, params, query, fragment] = request.split_uri() + + if request.command in ('post', 'put'): + request.collector = collector (self, request) + else: + request.error (400) + + def continue_request (self, data, request): + params, method = xmlrpclib.loads (data) + try: + # generate response + try: + response = self.call (method, params) + if type(response) != type(()): + response = (response,) + except: + # report exception back to server + response = xmlrpclib.dumps ( + xmlrpclib.Fault (1, "%s:%s" % (sys.exc_type, sys.exc_value)) + ) + else: + response = xmlrpclib.dumps (response, methodresponse=1) + except: + # internal error, report as HTTP server error + request.error (500) + else: + # got a valid XML RPC response + request['Content-Type'] = 'text/xml' + request.push (response) + request.done() + + def call (self, method, params): + # override this method to implement RPC methods + raise "NotYetImplemented" + +class collector: + + "gathers input for POST and PUT requests" + + def __init__ (self, handler, request): + + self.handler = handler + self.request = request + self.data = '' + + # make sure there's a content-length header + cl = request.get_header ('content-length') + + if not cl: + request.error (411) + else: + cl = string.atoi (cl) + # using a 'numeric' terminator + self.request.channel.set_terminator (cl) + + def collect_incoming_data (self, data): + self.data = self.data + data + + def found_terminator (self): + # set the terminator back to the default + self.request.channel.set_terminator ('\r\n\r\n') + self.handler.continue_request (self.data, self.request) + +if __name__ == '__main__': + + class rpc_demo (xmlrpc_handler): + + def call (self, method, params): + print 'method="%s" params=%s' % (method, params) + return "Sure, that works" + + import asyncore + import http_server + + hs = http_server.http_server ('', 8000) + rpc = rpc_demo() + hs.install_handler (rpc) + + asyncore.loop() diff --git a/demo/medusa054/00_README b/demo/medusa054/00_README new file mode 100644 index 0000000..5701364 --- /dev/null +++ b/demo/medusa054/00_README @@ -0,0 +1,30 @@ + + 21 Mar 2004 +------------- + +M2Crypto HTTPS and FTP/TLS servers + +All the files in this directory are from Medusa 0.54, except for the +following: + +- 00_README (this file) +- server.pem, the server's certificate +- ca.pem, my CA certificate +- https_server.py +- ftps_server.py +- START.py +- START_xmlrpc.py +- index.html, a sample HTML file +- poison_handler.py, a webpoison clone + +By default, http_server listens on port 39080 and https_server port +39443. Document root is current directory, and serves up index.html. + +The xmlrpc server is accessible below '/RPC2'. + +The FTP/TLS server listens on port 39021 by default. I've only tested it with +the 'anonymous' authentication handler. + +Medusa files are copyright Sam Rushing. Recent versions are maintained +by Andrew Kuchling. My files are copyright me. + diff --git a/demo/medusa054/START.py b/demo/medusa054/START.py new file mode 100644 index 0000000..fb08ab3 --- /dev/null +++ b/demo/medusa054/START.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python + +# Standard Python library +import os +import os.path +import sys + +# Medusa +import asyncore +import default_handler +import filesys +import ftp_server +import http_server +import status_handler + +# M2Crypto +import https_server +import poison_handler +import ftps_server +from M2Crypto import Rand, SSL, threading + +HTTP_PORT=39080 +HTTPS_PORT=39443 +FTP_PORT = 39021 + +hs=http_server.http_server('', HTTP_PORT) + +Rand.load_file('../randpool.dat', -1) +ssl_ctx=SSL.Context('sslv23') +ssl_ctx.load_cert('server.pem') +ssl_ctx.load_verify_locations('ca.pem', '') +ssl_ctx.load_client_CA('ca.pem') +#ssl_ctx.set_verify(SSL.verify_peer, 10) +#ssl_ctx.set_verify(SSL.verify_peer|SSL.verify_fail_if_no_peer_cert, 10) +#ssl_ctx.set_verify(SSL.verify_peer|SSL.verify_client_once, 10) +ssl_ctx.set_verify(SSL.verify_none, 10) +ssl_ctx.set_session_id_ctx('127.0.0.1:39443') +ssl_ctx.set_tmp_dh('dh1024.pem') +ssl_ctx.set_info_callback() + +hss=https_server.https_server('', HTTPS_PORT, ssl_ctx) + +fs=filesys.os_filesystem(os.path.abspath(os.curdir)) +#fs=filesys.os_filesystem('/usr/local/pkg/apache/htdocs') +#fs=filesys.os_filesystem('c:/pkg/jdk130/docs') +dh=default_handler.default_handler(fs) +hs.install_handler(dh) +hss.install_handler(dh) + +#class rpc_demo (xmlrpc_handler.xmlrpc_handler): +# def call (self, method, params): +# print 'method="%s" params=%s' % (method, params) +# return "Sure, that works" +#rpch = rpc_demo() +#hs.install_handler(rpch) +#hss.install_handler(rpch) + +ph=poison_handler.poison_handler(10) +hs.install_handler(ph) +hss.install_handler(ph) + +fauthz = ftp_server.anon_authorizer('/usr/local/pkg/apache/htdocs') +ftps = ftps_server.ftp_tls_server(fauthz, ssl_ctx, port=FTP_PORT) + +sh=status_handler.status_extension([hs, hss, ftps]) +hs.install_handler(sh) +hss.install_handler(sh) + +asyncore.loop() +Rand.save_file('../randpool.dat') + diff --git a/demo/medusa054/START_xmlrpc.py b/demo/medusa054/START_xmlrpc.py new file mode 100644 index 0000000..65b6d67 --- /dev/null +++ b/demo/medusa054/START_xmlrpc.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python + +# Standard Python library +import os +import os.path +import sys + +# Medusa +import asyncore +import default_handler +import filesys +import http_server +import status_handler + +# M2Crypto +import https_server +import poison_handler +from M2Crypto import Rand, SSL + +# XMLrpc +import xmlrpc_handler + + +HTTP_PORT=39080 +HTTPS_PORT=39443 + +hs=http_server.http_server('', HTTP_PORT) + +Rand.load_file('../randpool.dat', -1) +ssl_ctx=SSL.Context('sslv23') +ssl_ctx.load_cert('server.pem') +#ssl_ctx.load_verify_location('ca.pem') +#ssl_ctx.load_client_CA('ca.pem') +#ssl_ctx.set_verify(SSL.verify_peer, 10) +#ssl_ctx.set_verify(SSL.verify_peer|SSL.verify_fail_if_no_peer_cert, 10) +#ssl_ctx.set_verify(SSL.verify_peer|SSL.verify_client_once, 10) +ssl_ctx.set_verify(SSL.verify_none, 10) +ssl_ctx.set_session_id_ctx('127.0.0.1:9443') +ssl_ctx.set_tmp_dh('dh1024.pem') +#ssl_ctx.set_info_callback() + +hss=https_server.https_server('', HTTPS_PORT, ssl_ctx) + +fs=filesys.os_filesystem(os.path.abspath(os.curdir)) +#fs=filesys.os_filesystem('/usr/local/pkg/apache/htdocs') +#fs=filesys.os_filesystem('c:/pkg/jdk118/docs') +dh=default_handler.default_handler(fs) +hs.install_handler(dh) +hss.install_handler(dh) + +# Cribbed from xmlrpc_handler.py. +# This is where you implement your RPC functionality. +class rpc_demo (xmlrpc_handler.xmlrpc_handler): + def call (self, method, params): + print 'method="%s" params=%s' % (method, params) + return "Sure, that works" + +rpch = rpc_demo() +hs.install_handler(rpch) +hss.install_handler(rpch) + +ph=poison_handler.poison_handler(10) +hs.install_handler(ph) +hss.install_handler(ph) + +sh=status_handler.status_extension([hss]) +hs.install_handler(sh) +hss.install_handler(sh) + +asyncore.loop() +Rand.save_file('../randpool.dat') + diff --git a/demo/medusa054/ca.pem b/demo/medusa054/ca.pem new file mode 100644 index 0000000..b7c84a1 --- /dev/null +++ b/demo/medusa054/ca.pem @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDWTCCAsKgAwIBAgIBADANBgkqhkiG9w0BAQQFADCBgDELMAkGA1UEBhMCU0cx +ETAPBgNVBAoTCE0yQ3J5cHRvMRQwEgYDVQQLEwtNMkNyeXB0byBDQTEkMCIGA1UE +AxMbTTJDcnlwdG8gQ2VydGlmaWNhdGUgTWFzdGVyMSIwIAYJKoZIhvcNAQkBFhNu +Z3BzQG5ldG1lbWV0aWMuY29tMB4XDTAxMTIxNTA1NTU0NloXDTA0MTIxNDA1NTU0 +NlowgYAxCzAJBgNVBAYTAlNHMREwDwYDVQQKEwhNMkNyeXB0bzEUMBIGA1UECxML +TTJDcnlwdG8gQ0ExJDAiBgNVBAMTG00yQ3J5cHRvIENlcnRpZmljYXRlIE1hc3Rl +cjEiMCAGCSqGSIb3DQEJARYTbmdwc0BuZXRtZW1ldGljLmNvbTCBnzANBgkqhkiG +9w0BAQEFAAOBjQAwgYkCgYEAx8soJbS719LHK62VVVIQeC3oW0HvFArwPnA0LuEK +q+LaqMOJg1rS7hvFdX03diV+XJw7cC0iECZYJNG4ii1xbY6KRmufkInaAwm54E3N +e+YYVocaqUkcN6xVf6fwnLfPXbpFS/K2Umg11ObKMmi80JmiIdjcjRRCQZC7g1hf +q+kCAwEAAaOB4DCB3TAdBgNVHQ4EFgQU6/qcBzEtQphfXLhiOHbt2KqBwMIwga0G +A1UdIwSBpTCBooAU6/qcBzEtQphfXLhiOHbt2KqBwMKhgYakgYMwgYAxCzAJBgNV +BAYTAlNHMREwDwYDVQQKEwhNMkNyeXB0bzEUMBIGA1UECxMLTTJDcnlwdG8gQ0Ex +JDAiBgNVBAMTG00yQ3J5cHRvIENlcnRpZmljYXRlIE1hc3RlcjEiMCAGCSqGSIb3 +DQEJARYTbmdwc0BuZXRtZW1ldGljLmNvbYIBADAMBgNVHRMEBTADAQH/MA0GCSqG +SIb3DQEBBAUAA4GBAD+I14GuS5vJmyv1k7mUMbAicsWRHZ+zrGOq9L/L2LsA+lKQ +dAzEZE2+Zv8LBPJVltbJJhcFNJS/ZMAjEm4xlJuCpvXVMxd/M5AM29aqekWlIK7J +vsdDL8IuzpRkMniUiNKPhmB6IPIOslvUKx6QofcE0wDh6pg4VvIbCjkpZ7gf +-----END CERTIFICATE----- diff --git a/demo/medusa054/counter.py b/demo/medusa054/counter.py new file mode 100644 index 0000000..9dae67c --- /dev/null +++ b/demo/medusa054/counter.py @@ -0,0 +1,51 @@ +# -*- Mode: Python -*- + +# It is tempting to add an __int__ method to this class, but it's not +# a good idea. This class tries to gracefully handle integer +# overflow, and to hide this detail from both the programmer and the +# user. Note that the __str__ method can be relied on for printing out +# the value of a counter: +# +# >>> print 'Total Client: %s' % self.total_clients +# +# If you need to do arithmetic with the value, then use the 'as_long' +# method, the use of long arithmetic is a reminder that the counter +# will overflow. + +class counter: + "general-purpose counter" + + def __init__ (self, initial_value=0): + self.value = initial_value + + def increment (self, delta=1): + result = self.value + try: + self.value = self.value + delta + except OverflowError: + self.value = long(self.value) + delta + return result + + def decrement (self, delta=1): + result = self.value + try: + self.value = self.value - delta + except OverflowError: + self.value = long(self.value) - delta + return result + + def as_long (self): + return long(self.value) + + def __nonzero__ (self): + return self.value != 0 + + def __repr__ (self): + return '<counter value=%s at %x>' % (self.value, id(self)) + + def __str__ (self): + s = str(long(self.value)) + if s[-1:] == 'L': + s = s[:-1] + return s + diff --git a/demo/medusa054/default_handler.py b/demo/medusa054/default_handler.py new file mode 100644 index 0000000..9d5e9d7 --- /dev/null +++ b/demo/medusa054/default_handler.py @@ -0,0 +1,213 @@ +# -*- Mode: Python -*- +# +# Author: Sam Rushing <rushing@nightmare.com> +# Copyright 1997 by Sam Rushing +# All Rights Reserved. +# + +# standard python modules +import mimetypes +import re +import stat +import string + +# medusa modules +import http_date +import http_server +import status_handler +import producers + +unquote = http_server.unquote + +# This is the 'default' handler. it implements the base set of +# features expected of a simple file-delivering HTTP server. file +# services are provided through a 'filesystem' object, the very same +# one used by the FTP server. +# +# You can replace or modify this handler if you want a non-standard +# HTTP server. You can also derive your own handler classes from +# it. +# +# support for handling POST requests is available in the derived +# class <default_with_post_handler>, defined below. +# + +from counter import counter + +class default_handler: + + valid_commands = ['GET', 'HEAD'] + + IDENT = 'Default HTTP Request Handler' + + # Pathnames that are tried when a URI resolves to a directory name + directory_defaults = [ + 'index.html', + 'default.html' + ] + + default_file_producer = producers.file_producer + + def __init__ (self, filesystem): + self.filesystem = filesystem + # count total hits + self.hit_counter = counter() + # count file deliveries + self.file_counter = counter() + # count cache hits + self.cache_counter = counter() + + hit_counter = 0 + + def __repr__ (self): + return '<%s (%s hits) at %x>' % ( + self.IDENT, + self.hit_counter, + id (self) + ) + + # always match, since this is a default + def match (self, request): + return 1 + + # handle a file request, with caching. + + def handle_request (self, request): + + if request.command not in self.valid_commands: + request.error (400) # bad request + return + + self.hit_counter.increment() + + path, params, query, fragment = request.split_uri() + + if '%' in path: + path = unquote (path) + + # strip off all leading slashes + while path and path[0] == '/': + path = path[1:] + + if self.filesystem.isdir (path): + if path and path[-1] != '/': + request['Location'] = 'http://%s/%s/' % ( + request.channel.server.server_name, + path + ) + request.error (301) + return + + # we could also generate a directory listing here, + # may want to move this into another method for that + # purpose + found = 0 + if path and path[-1] != '/': + path = path + '/' + for default in self.directory_defaults: + p = path + default + if self.filesystem.isfile (p): + path = p + found = 1 + break + if not found: + request.error (404) # Not Found + return + + elif not self.filesystem.isfile (path): + request.error (404) # Not Found + return + + file_length = self.filesystem.stat (path)[stat.ST_SIZE] + + ims = get_header_match (IF_MODIFIED_SINCE, request.header) + + length_match = 1 + if ims: + length = ims.group (4) + if length: + try: + length = string.atoi (length) + if length != file_length: + length_match = 0 + except: + pass + + ims_date = 0 + + if ims: + ims_date = http_date.parse_http_date (ims.group (1)) + + try: + mtime = self.filesystem.stat (path)[stat.ST_MTIME] + except: + request.error (404) + return + + if length_match and ims_date: + if mtime <= ims_date: + request.reply_code = 304 + request.done() + self.cache_counter.increment() + return + try: + file = self.filesystem.open (path, 'rb') + except IOError: + request.error (404) + return + + request['Last-Modified'] = http_date.build_http_date (mtime) + request['Content-Length'] = file_length + self.set_content_type (path, request) + + if request.command == 'GET': + request.push (self.default_file_producer (file)) + + self.file_counter.increment() + request.done() + + def set_content_type (self, path, request): + ext = string.lower (get_extension (path)) + typ, encoding = mimetypes.guess_type(path) + if typ is not None: + request['Content-Type'] = typ + else: + # TODO: test a chunk off the front of the file for 8-bit + # characters, and use application/octet-stream instead. + request['Content-Type'] = 'text/plain' + + def status (self): + return producers.simple_producer ( + '<li>%s' % status_handler.html_repr (self) + + '<ul>' + + ' <li><b>Total Hits:</b> %s' % self.hit_counter + + ' <li><b>Files Delivered:</b> %s' % self.file_counter + + ' <li><b>Cache Hits:</b> %s' % self.cache_counter + + '</ul>' + ) + +# HTTP/1.0 doesn't say anything about the "; length=nnnn" addition +# to this header. I suppose its purpose is to avoid the overhead +# of parsing dates... +IF_MODIFIED_SINCE = re.compile ( + 'If-Modified-Since: ([^;]+)((; length=([0-9]+)$)|$)', + re.IGNORECASE + ) + +USER_AGENT = re.compile ('User-Agent: (.*)', re.IGNORECASE) + +CONTENT_TYPE = re.compile ( + r'Content-Type: ([^;]+)((; boundary=([A-Za-z0-9\'\(\)+_,./:=?-]+)$)|$)', + re.IGNORECASE + ) + +get_header = http_server.get_header +get_header_match = http_server.get_header_match + +def get_extension (path): + dirsep = string.rfind (path, '/') + dotsep = string.rfind (path, '.') + if dotsep > dirsep: + return path[dotsep+1:] + else: + return '' diff --git a/demo/medusa054/dh1024.pem b/demo/medusa054/dh1024.pem new file mode 100644 index 0000000..81d43f6 --- /dev/null +++ b/demo/medusa054/dh1024.pem @@ -0,0 +1,5 @@ +-----BEGIN DH PARAMETERS----- +MIGHAoGBAJf2QmHKtQXdKCjhPx1ottPb0PMTBH9A6FbaWMsTuKG/K3g6TG1Z1fkq +/Gz/PWk/eLI9TzFgqVAuPvr3q14a1aZeVUMTgo2oO5/y2UHe6VaJ+trqCTat3xlx +/mNbIK9HA2RgPC3gWfVLZQrY+gz3ASHHR5nXWHEyvpuZm7m3h+irAgEC +-----END DH PARAMETERS----- diff --git a/demo/medusa054/filesys.py b/demo/medusa054/filesys.py new file mode 100644 index 0000000..1affd8f --- /dev/null +++ b/demo/medusa054/filesys.py @@ -0,0 +1,394 @@ +# -*- Mode: Python -*- +# $Id: filesys.py 299 2005-06-09 17:32:28Z heikki $ +# Author: Sam Rushing <rushing@nightmare.com> +# +# Generic filesystem interface. +# + +# We want to provide a complete wrapper around any and all +# filesystem operations. + +# this class is really just for documentation, +# identifying the API for a filesystem object. + +# opening files for reading, and listing directories, should +# return a producer. + +class abstract_filesystem: + def __init__ (self): + pass + + def current_directory (self): + "Return a string representing the current directory." + pass + + def listdir (self, path, long=0): + """Return a listing of the directory at 'path' The empty string + indicates the current directory. If 'long' is set, instead + return a list of (name, stat_info) tuples + """ + pass + + def open (self, path, mode): + "Return an open file object" + pass + + def stat (self, path): + "Return the equivalent of os.stat() on the given path." + pass + + def isdir (self, path): + "Does the path represent a directory?" + pass + + def isfile (self, path): + "Does the path represent a plain file?" + pass + + def cwd (self, path): + "Change the working directory." + pass + + def cdup (self): + "Change to the parent of the current directory." + pass + + + def longify (self, path): + """Return a 'long' representation of the filename + [for the output of the LIST command]""" + pass + +# standard wrapper around a unix-like filesystem, with a 'false root' +# capability. + +# security considerations: can symbolic links be used to 'escape' the +# root? should we allow it? if not, then we could scan the +# filesystem on startup, but that would not help if they were added +# later. We will probably need to check for symlinks in the cwd method. + +# what to do if wd is an invalid directory? + +import os +import stat +import re +import string + +def safe_stat (path): + try: + return (path, os.stat (path)) + except: + return None + +import glob + +class os_filesystem: + path_module = os.path + + # set this to zero if you want to disable pathname globbing. + # [we currently don't glob, anyway] + do_globbing = 1 + + def __init__ (self, root, wd='/'): + self.root = root + self.wd = wd + + def current_directory (self): + return self.wd + + def isfile (self, path): + p = self.normalize (self.path_module.join (self.wd, path)) + return self.path_module.isfile (self.translate(p)) + + def isdir (self, path): + p = self.normalize (self.path_module.join (self.wd, path)) + return self.path_module.isdir (self.translate(p)) + + def cwd (self, path): + p = self.normalize (self.path_module.join (self.wd, path)) + translated_path = self.translate(p) + if not self.path_module.isdir (translated_path): + return 0 + else: + old_dir = os.getcwd() + # temporarily change to that directory, in order + # to see if we have permission to do so. + try: + can = 0 + try: + os.chdir (translated_path) + can = 1 + self.wd = p + except: + pass + finally: + if can: + os.chdir (old_dir) + return can + + def cdup (self): + return self.cwd ('..') + + def listdir (self, path, long=0): + p = self.translate (path) + # I think we should glob, but limit it to the current + # directory only. + ld = os.listdir (p) + if not long: + return list_producer (ld, None) + else: + old_dir = os.getcwd() + try: + os.chdir (p) + # if os.stat fails we ignore that file. + result = filter (None, map (safe_stat, ld)) + finally: + os.chdir (old_dir) + return list_producer (result, self.longify) + + # TODO: implement a cache w/timeout for stat() + def stat (self, path): + p = self.translate (path) + return os.stat (p) + + def open (self, path, mode): + p = self.translate (path) + return open (p, mode) + + def unlink (self, path): + p = self.translate (path) + return os.unlink (p) + + def mkdir (self, path): + p = self.translate (path) + return os.mkdir (p) + + def rmdir (self, path): + p = self.translate (path) + return os.rmdir (p) + + # utility methods + def normalize (self, path): + # watch for the ever-sneaky '/+' path element + path = re.sub('/+', '/', path) + p = self.path_module.normpath (path) + # remove 'dangling' cdup's. + if len(p) > 2 and p[:3] == '/..': + p = '/' + return p + + def translate (self, path): + # we need to join together three separate + # path components, and do it safely. + # <real_root>/<current_directory>/<path> + # use the operating system's path separator. + path = string.join (string.split (path, '/'), os.sep) + p = self.normalize (self.path_module.join (self.wd, path)) + p = self.normalize (self.path_module.join (self.root, p[1:])) + return p + + def longify (self, (path, stat_info)): + return unix_longify (path, stat_info) + + def __repr__ (self): + return '<unix-style fs root:%s wd:%s>' % ( + self.root, + self.wd + ) + +if os.name == 'posix': + + class unix_filesystem (os_filesystem): + pass + + class schizophrenic_unix_filesystem (os_filesystem): + PROCESS_UID = os.getuid() + PROCESS_EUID = os.geteuid() + PROCESS_GID = os.getgid() + PROCESS_EGID = os.getegid() + + def __init__ (self, root, wd='/', persona=(None, None)): + os_filesystem.__init__ (self, root, wd) + self.persona = persona + + def become_persona (self): + if self.persona is not (None, None): + uid, gid = self.persona + # the order of these is important! + os.setegid (gid) + os.seteuid (uid) + + def become_nobody (self): + if self.persona is not (None, None): + os.seteuid (self.PROCESS_UID) + os.setegid (self.PROCESS_GID) + + # cwd, cdup, open, listdir + def cwd (self, path): + try: + self.become_persona() + return os_filesystem.cwd (self, path) + finally: + self.become_nobody() + + def cdup (self, path): + try: + self.become_persona() + return os_filesystem.cdup (self) + finally: + self.become_nobody() + + def open (self, filename, mode): + try: + self.become_persona() + return os_filesystem.open (self, filename, mode) + finally: + self.become_nobody() + + def listdir (self, path, long=0): + try: + self.become_persona() + return os_filesystem.listdir (self, path, long) + finally: + self.become_nobody() + +# For the 'real' root, we could obtain a list of drives, and then +# use that. Doesn't win32 provide such a 'real' filesystem? +# [yes, I think something like this "\\.\c\windows"] + +class msdos_filesystem (os_filesystem): + def longify (self, (path, stat_info)): + return msdos_longify (path, stat_info) + +# A merged filesystem will let you plug other filesystems together. +# We really need the equivalent of a 'mount' capability - this seems +# to be the most general idea. So you'd use a 'mount' method to place +# another filesystem somewhere in the hierarchy. + +# Note: this is most likely how I will handle ~user directories +# with the http server. + +class merged_filesystem: + def __init__ (self, *fsys): + pass + +# this matches the output of NT's ftp server (when in +# MSDOS mode) exactly. + +def msdos_longify (file, stat_info): + if stat.S_ISDIR (stat_info[stat.ST_MODE]): + dir = '<DIR>' + else: + dir = ' ' + date = msdos_date (stat_info[stat.ST_MTIME]) + return '%s %s %8d %s' % ( + date, + dir, + stat_info[stat.ST_SIZE], + file + ) + +def msdos_date (t): + try: + info = time.gmtime (t) + except: + info = time.gmtime (0) + # year, month, day, hour, minute, second, ... + if info[3] > 11: + merid = 'PM' + info[3] = info[3] - 12 + else: + merid = 'AM' + return '%02d-%02d-%02d %02d:%02d%s' % ( + info[1], + info[2], + info[0]%100, + info[3], + info[4], + merid + ) + +months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', + 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] + +mode_table = { + '0':'---', + '1':'--x', + '2':'-w-', + '3':'-wx', + '4':'r--', + '5':'r-x', + '6':'rw-', + '7':'rwx' + } + +import time + +def unix_longify (file, stat_info): + # for now, only pay attention to the lower bits + mode = ('%o' % stat_info[stat.ST_MODE])[-3:] + mode = string.join (map (lambda x: mode_table[x], mode), '') + if stat.S_ISDIR (stat_info[stat.ST_MODE]): + dirchar = 'd' + else: + dirchar = '-' + date = ls_date (long(time.time()), stat_info[stat.ST_MTIME]) + return '%s%s %3d %-8d %-8d %8d %s %s' % ( + dirchar, + mode, + stat_info[stat.ST_NLINK], + stat_info[stat.ST_UID], + stat_info[stat.ST_GID], + stat_info[stat.ST_SIZE], + date, + file + ) + +# Emulate the unix 'ls' command's date field. +# it has two formats - if the date is more than 180 +# days in the past, then it's like this: +# Oct 19 1995 +# otherwise, it looks like this: +# Oct 19 17:33 + +def ls_date (now, t): + try: + info = time.gmtime (t) + except: + info = time.gmtime (0) + # 15,600,000 == 86,400 * 180 + if (now - t) > 15600000: + return '%s %2d %d' % ( + months[info[1]-1], + info[2], + info[0] + ) + else: + return '%s %2d %02d:%02d' % ( + months[info[1]-1], + info[2], + info[3], + info[4] + ) + +# =========================================================================== +# Producers +# =========================================================================== + +class list_producer: + def __init__ (self, list, func=None): + self.list = list + self.func = func + + # this should do a pushd/popd + def more (self): + if not self.list: + return '' + else: + # do a few at a time + bunch = self.list[:50] + if self.func is not None: + bunch = map (self.func, bunch) + self.list = self.list[50:] + return string.joinfields (bunch, '\r\n') + '\r\n' + diff --git a/demo/medusa054/ftp_server.py b/demo/medusa054/ftp_server.py new file mode 100644 index 0000000..859cd65 --- /dev/null +++ b/demo/medusa054/ftp_server.py @@ -0,0 +1,1111 @@ +# -*- Mode: Python -*- + +# Author: Sam Rushing <rushing@nightmare.com> +# Copyright 1996-2000 by Sam Rushing +# All Rights Reserved. +# + +# An extensible, configurable, asynchronous FTP server. +# +# All socket I/O is non-blocking, however file I/O is currently +# blocking. Eventually file I/O may be made non-blocking, too, if it +# seems necessary. Currently the only CPU-intensive operation is +# getting and formatting a directory listing. [this could be moved +# into another process/directory server, or another thread?] +# +# Only a subset of RFC 959 is implemented, but much of that RFC is +# vestigial anyway. I've attempted to include the most commonly-used +# commands, using the feature set of wu-ftpd as a guide. + +import asyncore +import asynchat + +import os +import socket +import stat +import string +import sys +import time + +#from medusa.producers import file_producer +from producers import file_producer + +# TODO: implement a directory listing cache. On very-high-load +# servers this could save a lot of disk abuse, and possibly the +# work of computing emulated unix ls output. + +# Potential security problem with the FTP protocol? I don't think +# there's any verification of the origin of a data connection. Not +# really a problem for the server (since it doesn't send the port +# command, except when in PASV mode) But I think a data connection +# could be spoofed by a program with access to a sniffer - it could +# watch for a PORT command to go over a command channel, and then +# connect to that port before the server does. + +# Unix user id's: +# In order to support assuming the id of a particular user, +# it seems there are two options: +# 1) fork, and seteuid in the child +# 2) carefully control the effective uid around filesystem accessing +# methods, using try/finally. [this seems to work] + +VERSION = '1.1' + +from counter import counter +import producers +import status_handler +import logger + +class ftp_channel (asynchat.async_chat): + + # defaults for a reliable __repr__ + addr = ('unknown','0') + + # unset this in a derived class in order + # to enable the commands in 'self.write_commands' + read_only = 1 + write_commands = ['appe','dele','mkd','rmd','rnfr','rnto','stor','stou'] + + restart_position = 0 + + # comply with (possibly troublesome) RFC959 requirements + # This is necessary to correctly run an active data connection + # through a firewall that triggers on the source port (expected + # to be 'L-1', or 20 in the normal case). + bind_local_minus_one = 0 + + def __init__ (self, server, conn, addr): + self.server = server + self.current_mode = 'a' + self.addr = addr + asynchat.async_chat.__init__ (self, conn) + self.set_terminator ('\r\n') + + # client data port. Defaults to 'the same as the control connection'. + self.client_addr = (addr[0], 21) + + self.client_dc = None + self.in_buffer = '' + self.closing = 0 + self.passive_acceptor = None + self.passive_connection = None + self.filesystem = None + self.authorized = 0 + # send the greeting + self.respond ( + '220 %s FTP server (Medusa Async V%s [experimental]) ready.' % ( + self.server.hostname, + VERSION + ) + ) + +# def __del__ (self): +# print 'ftp_channel.__del__()' + + # -------------------------------------------------- + # async-library methods + # -------------------------------------------------- + + def handle_expt (self): + # this is handled below. not sure what I could + # do here to make that code less kludgish. + pass + + def collect_incoming_data (self, data): + self.in_buffer = self.in_buffer + data + if len(self.in_buffer) > 4096: + # silently truncate really long lines + # (possible denial-of-service attack) + self.in_buffer = '' + + def found_terminator (self): + + line = self.in_buffer + + if not len(line): + return + + sp = string.find (line, ' ') + if sp != -1: + line = [line[:sp], line[sp+1:]] + else: + line = [line] + + command = string.lower (line[0]) + # watch especially for 'urgent' abort commands. + if string.find (command, 'abor') != -1: + # strip off telnet sync chars and the like... + while command and command[0] not in string.letters: + command = command[1:] + fun_name = 'cmd_%s' % command + if command != 'pass': + self.log ('<== %s' % repr(self.in_buffer)[1:-1]) + else: + self.log ('<== %s' % line[0]+' <password>') + self.in_buffer = '' + if not hasattr (self, fun_name): + self.command_not_understood (line[0]) + return + fun = getattr (self, fun_name) + if (not self.authorized) and (command not in ('user', 'pass', 'help', 'quit')): + self.respond ('530 Please log in with USER and PASS') + elif (not self.check_command_authorization (command)): + self.command_not_authorized (command) + else: + try: + result = apply (fun, (line,)) + except: + self.server.total_exceptions.increment() + (file, fun, line), t,v, tbinfo = asyncore.compact_traceback() + if self.client_dc: + try: + self.client_dc.close() + except: + pass + self.respond ( + '451 Server Error: %s, %s: file: %s line: %s' % ( + t,v,file,line, + ) + ) + + closed = 0 + def close (self): + if not self.closed: + self.closed = 1 + if self.passive_acceptor: + self.passive_acceptor.close() + if self.client_dc: + self.client_dc.close() + self.server.closed_sessions.increment() + asynchat.async_chat.close (self) + + # -------------------------------------------------- + # filesystem interface functions. + # override these to provide access control or perform + # other functions. + # -------------------------------------------------- + + def cwd (self, line): + return self.filesystem.cwd (line[1]) + + def cdup (self, line): + return self.filesystem.cdup() + + def open (self, path, mode): + return self.filesystem.open (path, mode) + + # returns a producer + def listdir (self, path, long=0): + return self.filesystem.listdir (path, long) + + def get_dir_list (self, line, long=0): + # we need to scan the command line for arguments to '/bin/ls'... + args = line[1:] + path_args = [] + for arg in args: + if arg[0] != '-': + path_args.append (arg) + else: + # ignore arguments + pass + if len(path_args) < 1: + dir = '.' + else: + dir = path_args[0] + return self.listdir (dir, long) + + # -------------------------------------------------- + # authorization methods + # -------------------------------------------------- + + def check_command_authorization (self, command): + if command in self.write_commands and self.read_only: + return 0 + else: + return 1 + + # -------------------------------------------------- + # utility methods + # -------------------------------------------------- + + def log (self, message): + self.server.logger.log ( + self.addr[0], + '%d %s' % ( + self.addr[1], message + ) + ) + + def respond (self, resp): + self.log ('==> %s' % resp) + self.push (resp + '\r\n') + + def command_not_understood (self, command): + self.respond ("500 '%s': command not understood." % command) + + def command_not_authorized (self, command): + self.respond ( + "530 You are not authorized to perform the '%s' command" % ( + command + ) + ) + + def make_xmit_channel (self): + # In PASV mode, the connection may or may _not_ have been made + # yet. [although in most cases it is... FTP Explorer being + # the only exception I've yet seen]. This gets somewhat confusing + # because things may happen in any order... + pa = self.passive_acceptor + if pa: + if pa.ready: + # a connection has already been made. + conn, addr = self.passive_acceptor.ready + cdc = xmit_channel (self, addr) + cdc.set_socket (conn) + cdc.connected = 1 + self.passive_acceptor.close() + self.passive_acceptor = None + else: + # we're still waiting for a connect to the PASV port. + cdc = xmit_channel (self) + else: + # not in PASV mode. + ip, port = self.client_addr + cdc = xmit_channel (self, self.client_addr) + cdc.create_socket (socket.AF_INET, socket.SOCK_STREAM) + if self.bind_local_minus_one: + cdc.bind (('', self.server.port - 1)) + try: + cdc.connect ((ip, port)) + except socket.error, why: + self.respond ("425 Can't build data connection") + self.client_dc = cdc + + # pretty much the same as xmit, but only right on the verge of + # being worth a merge. + def make_recv_channel (self, fd): + pa = self.passive_acceptor + if pa: + if pa.ready: + # a connection has already been made. + conn, addr = pa.ready + cdc = recv_channel (self, addr, fd) + cdc.set_socket (conn) + cdc.connected = 1 + self.passive_acceptor.close() + self.passive_acceptor = None + else: + # we're still waiting for a connect to the PASV port. + cdc = recv_channel (self, None, fd) + else: + # not in PASV mode. + ip, port = self.client_addr + cdc = recv_channel (self, self.client_addr, fd) + cdc.create_socket (socket.AF_INET, socket.SOCK_STREAM) + try: + cdc.connect ((ip, port)) + except socket.error, why: + self.respond ("425 Can't build data connection") + self.client_dc = cdc + + type_map = { + 'a':'ASCII', + 'i':'Binary', + 'e':'EBCDIC', + 'l':'Binary' + } + + type_mode_map = { + 'a':'t', + 'i':'b', + 'e':'b', + 'l':'b' + } + + # -------------------------------------------------- + # command methods + # -------------------------------------------------- + + def cmd_type (self, line): + 'specify data transfer type' + # ascii, ebcdic, image, local <byte size> + t = string.lower (line[1]) + # no support for EBCDIC + # if t not in ['a','e','i','l']: + if t not in ['a','i','l']: + self.command_not_understood (string.join (line)) + elif t == 'l' and (len(line) > 2 and line[2] != '8'): + self.respond ('504 Byte size must be 8') + else: + self.current_mode = t + self.respond ('200 Type set to %s.' % self.type_map[t]) + + + def cmd_quit (self, line): + 'terminate session' + self.respond ('221 Goodbye.') + self.close_when_done() + + def cmd_port (self, line): + 'specify data connection port' + info = string.split (line[1], ',') + ip = string.join (info[:4], '.') + port = string.atoi(info[4])*256 + string.atoi(info[5]) + # how many data connections at a time? + # I'm assuming one for now... + # TODO: we should (optionally) verify that the + # ip number belongs to the client. [wu-ftpd does this?] + self.client_addr = (ip, port) + self.respond ('200 PORT command successful.') + + def new_passive_acceptor (self): + # ensure that only one of these exists at a time. + if self.passive_acceptor is not None: + self.passive_acceptor.close() + self.passive_acceptor = None + self.passive_acceptor = passive_acceptor (self) + return self.passive_acceptor + + def cmd_pasv (self, line): + 'prepare for server-to-server transfer' + pc = self.new_passive_acceptor() + port = pc.addr[1] + ip_addr = pc.control_channel.getsockname()[0] + self.respond ( + '227 Entering Passive Mode (%s,%d,%d)' % ( + string.replace(ip_addr, '.', ','), + port/256, + port%256 + ) + ) + self.client_dc = None + + def cmd_nlst (self, line): + 'give name list of files in directory' + # ncftp adds the -FC argument for the user-visible 'nlist' + # command. We could try to emulate ls flags, but not just yet. + if '-FC' in line: + line.remove ('-FC') + try: + dir_list_producer = self.get_dir_list (line, 0) + except os.error, why: + self.respond ('550 Could not list directory: %s' % why) + return + self.respond ( + '150 Opening %s mode data connection for file list' % ( + self.type_map[self.current_mode] + ) + ) + self.make_xmit_channel() + self.client_dc.push_with_producer (dir_list_producer) + self.client_dc.close_when_done() + + def cmd_list (self, line): + 'give a list of files in a directory' + try: + dir_list_producer = self.get_dir_list (line, 1) + except os.error, why: + self.respond ('550 Could not list directory: %s' % why) + return + self.respond ( + '150 Opening %s mode data connection for file list' % ( + self.type_map[self.current_mode] + ) + ) + self.make_xmit_channel() + self.client_dc.push_with_producer (dir_list_producer) + self.client_dc.close_when_done() + + def cmd_cwd (self, line): + 'change working directory' + if self.cwd (line): + self.respond ('250 CWD command successful.') + else: + self.respond ('550 No such directory.') + + def cmd_cdup (self, line): + 'change to parent of current working directory' + if self.cdup(line): + self.respond ('250 CDUP command successful.') + else: + self.respond ('550 No such directory.') + + def cmd_pwd (self, line): + 'print the current working directory' + self.respond ( + '257 "%s" is the current directory.' % ( + self.filesystem.current_directory() + ) + ) + + # modification time + # example output: + # 213 19960301204320 + def cmd_mdtm (self, line): + 'show last modification time of file' + filename = line[1] + if not self.filesystem.isfile (filename): + self.respond ('550 "%s" is not a file' % filename) + else: + mtime = time.gmtime(self.filesystem.stat(filename)[stat.ST_MTIME]) + self.respond ( + '213 %4d%02d%02d%02d%02d%02d' % ( + mtime[0], + mtime[1], + mtime[2], + mtime[3], + mtime[4], + mtime[5] + ) + ) + + def cmd_noop (self, line): + 'do nothing' + self.respond ('200 NOOP command successful.') + + def cmd_size (self, line): + 'return size of file' + filename = line[1] + if not self.filesystem.isfile (filename): + self.respond ('550 "%s" is not a file' % filename) + else: + self.respond ( + '213 %d' % (self.filesystem.stat(filename)[stat.ST_SIZE]) + ) + + def cmd_retr (self, line): + 'retrieve a file' + if len(line) < 2: + self.command_not_understood (string.join (line)) + else: + file = line[1] + if not self.filesystem.isfile (file): + self.log_info ('checking %s' % file) + self.respond ('550 No such file') + else: + try: + # FIXME: for some reason, 'rt' isn't working on win95 + mode = 'r'+self.type_mode_map[self.current_mode] + fd = self.open (file, mode) + except IOError, why: + self.respond ('553 could not open file for reading: %s' % (repr(why))) + return + self.respond ( + "150 Opening %s mode data connection for file '%s'" % ( + self.type_map[self.current_mode], + file + ) + ) + self.make_xmit_channel() + + if self.restart_position: + # try to position the file as requested, but + # give up silently on failure (the 'file object' + # may not support seek()) + try: + fd.seek (self.restart_position) + except: + pass + self.restart_position = 0 + + self.client_dc.push_with_producer ( + file_producer (fd) + ) + self.client_dc.close_when_done() + + def cmd_stor (self, line, mode='wb'): + 'store a file' + if len (line) < 2: + self.command_not_understood (string.join (line)) + else: + if self.restart_position: + restart_position = 0 + self.respond ('553 restart on STOR not yet supported') + return + file = line[1] + # todo: handle that type flag + try: + fd = self.open (file, mode) + except IOError, why: + self.respond ('553 could not open file for writing: %s' % (repr(why))) + return + self.respond ( + '150 Opening %s connection for %s' % ( + self.type_map[self.current_mode], + file + ) + ) + self.make_recv_channel (fd) + + def cmd_abor (self, line): + 'abort operation' + if self.client_dc: + self.client_dc.close() + self.respond ('226 ABOR command successful.') + + def cmd_appe (self, line): + 'append to a file' + return self.cmd_stor (line, 'ab') + + def cmd_dele (self, line): + if len (line) != 2: + self.command_not_understood (string.join (line)) + else: + file = line[1] + if self.filesystem.isfile (file): + try: + self.filesystem.unlink (file) + self.respond ('250 DELE command successful.') + except: + self.respond ('550 error deleting file.') + else: + self.respond ('550 %s: No such file.' % file) + + def cmd_mkd (self, line): + if len (line) != 2: + self.command_not_understood (string.join (line)) + else: + path = line[1] + try: + self.filesystem.mkdir (path) + self.respond ('257 MKD command successful.') + except: + self.respond ('550 error creating directory.') + + def cmd_rmd (self, line): + if len (line) != 2: + self.command_not_understood (string.join (line)) + else: + path = line[1] + try: + self.filesystem.rmdir (path) + self.respond ('250 RMD command successful.') + except: + self.respond ('550 error removing directory.') + + def cmd_user (self, line): + 'specify user name' + if len(line) > 1: + self.user = line[1] + self.respond ('331 Password required.') + else: + self.command_not_understood (string.join (line)) + + def cmd_pass (self, line): + 'specify password' + if len(line) < 2: + pw = '' + else: + pw = line[1] + result, message, fs = self.server.authorizer.authorize (self, self.user, pw) + if result: + self.respond ('230 %s' % message) + self.filesystem = fs + self.authorized = 1 + self.log_info('Successful login: Filesystem=%s' % repr(fs)) + else: + self.respond ('530 %s' % message) + + def cmd_rest (self, line): + 'restart incomplete transfer' + try: + pos = string.atoi (line[1]) + except ValueError: + self.command_not_understood (string.join (line)) + self.restart_position = pos + self.respond ( + '350 Restarting at %d. Send STORE or RETRIEVE to initiate transfer.' % pos + ) + + def cmd_stru (self, line): + 'obsolete - set file transfer structure' + if line[1] in 'fF': + # f == 'file' + self.respond ('200 STRU F Ok') + else: + self.respond ('504 Unimplemented STRU type') + + def cmd_mode (self, line): + 'obsolete - set file transfer mode' + if line[1] in 'sS': + # f == 'file' + self.respond ('200 MODE S Ok') + else: + self.respond ('502 Unimplemented MODE type') + +# The stat command has two personalities. Normally it returns status +# information about the current connection. But if given an argument, +# it is equivalent to the LIST command, with the data sent over the +# control connection. Strange. But wuftpd, ftpd, and nt's ftp server +# all support it. +# +## def cmd_stat (self, line): +## 'return status of server' +## pass + + def cmd_syst (self, line): + 'show operating system type of server system' + # Replying to this command is of questionable utility, because + # this server does not behave in a predictable way w.r.t. the + # output of the LIST command. We emulate Unix ls output, but + # on win32 the pathname can contain drive information at the front + # Currently, the combination of ensuring that os.sep == '/' + # and removing the leading slash when necessary seems to work. + # [cd'ing to another drive also works] + # + # This is how wuftpd responds, and is probably + # the most expected. The main purpose of this reply is so that + # the client knows to expect Unix ls-style LIST output. + self.respond ('215 UNIX Type: L8') + # one disadvantage to this is that some client programs + # assume they can pass args to /bin/ls. + # a few typical responses: + # 215 UNIX Type: L8 (wuftpd) + # 215 Windows_NT version 3.51 + # 215 VMS MultiNet V3.3 + # 500 'SYST': command not understood. (SVR4) + + def cmd_help (self, line): + 'give help information' + # find all the methods that match 'cmd_xxxx', + # use their docstrings for the help response. + attrs = dir(self.__class__) + help_lines = [] + for attr in attrs: + if attr[:4] == 'cmd_': + x = getattr (self, attr) + if type(x) == type(self.cmd_help): + if x.__doc__: + help_lines.append ('\t%s\t%s' % (attr[4:], x.__doc__)) + if help_lines: + self.push ('214-The following commands are recognized\r\n') + self.push_with_producer (producers.lines_producer (help_lines)) + self.push ('214\r\n') + else: + self.push ('214-\r\n\tHelp Unavailable\r\n214\r\n') + +class ftp_server (asyncore.dispatcher): + # override this to spawn a different FTP channel class. + ftp_channel_class = ftp_channel + + SERVER_IDENT = 'FTP Server (V%s)' % VERSION + + def __init__ ( + self, + authorizer, + hostname =None, + ip ='', + port =21, + resolver =None, + logger_object=logger.file_logger (sys.stdout) + ): + self.ip = ip + self.port = port + self.authorizer = authorizer + + if hostname is None: + self.hostname = socket.gethostname() + else: + self.hostname = hostname + + # statistics + self.total_sessions = counter() + self.closed_sessions = counter() + self.total_files_out = counter() + self.total_files_in = counter() + self.total_bytes_out = counter() + self.total_bytes_in = counter() + self.total_exceptions = counter() + # + asyncore.dispatcher.__init__ (self) + self.create_socket (socket.AF_INET, socket.SOCK_STREAM) + + self.set_reuse_addr() + self.bind ((self.ip, self.port)) + self.listen (5) + + if not logger_object: + logger_object = sys.stdout + + if resolver: + self.logger = logger.resolving_logger (resolver, logger_object) + else: + self.logger = logger.unresolving_logger (logger_object) + + self.log_info('FTP server started at %s\n\tAuthorizer:%s\n\tHostname: %s\n\tPort: %d' % ( + time.ctime(time.time()), + repr (self.authorizer), + self.hostname, + self.port) + ) + + def writable (self): + return 0 + + def handle_read (self): + pass + + def handle_connect (self): + pass + + def handle_accept (self): + conn, addr = self.accept() + self.total_sessions.increment() + self.log_info('Incoming connection from %s:%d' % (addr[0], addr[1])) + self.ftp_channel_class (self, conn, addr) + + # return a producer describing the state of the server + def status (self): + + def nice_bytes (n): + return string.join (status_handler.english_bytes (n)) + + return producers.lines_producer ( + ['<h2>%s</h2>' % self.SERVER_IDENT, + '<br>Listening on <b>Host:</b> %s' % self.hostname, + '<b>Port:</b> %d' % self.port, + '<br>Sessions', + '<b>Total:</b> %s' % self.total_sessions, + '<b>Current:</b> %d' % (self.total_sessions.as_long() - self.closed_sessions.as_long()), + '<br>Files', + '<b>Sent:</b> %s' % self.total_files_out, + '<b>Received:</b> %s' % self.total_files_in, + '<br>Bytes', + '<b>Sent:</b> %s' % nice_bytes (self.total_bytes_out.as_long()), + '<b>Received:</b> %s' % nice_bytes (self.total_bytes_in.as_long()), + '<br>Exceptions: %s' % self.total_exceptions, + ] + ) + +# ====================================================================== +# Data Channel Classes +# ====================================================================== + +# This socket accepts a data connection, used when the server has been +# placed in passive mode. Although the RFC implies that we ought to +# be able to use the same acceptor over and over again, this presents +# a problem: how do we shut it off, so that we are accepting +# connections only when we expect them? [we can't] +# +# wuftpd, and probably all the other servers, solve this by allowing +# only one connection to hit this acceptor. They then close it. Any +# subsequent data-connection command will then try for the default +# port on the client side [which is of course never there]. So the +# 'always-send-PORT/PASV' behavior seems required. +# +# Another note: wuftpd will also be listening on the channel as soon +# as the PASV command is sent. It does not wait for a data command +# first. + +# --- we need to queue up a particular behavior: +# 1) xmit : queue up producer[s] +# 2) recv : the file object +# +# It would be nice if we could make both channels the same. Hmmm.. +# + +class passive_acceptor (asyncore.dispatcher): + ready = None + + def __init__ (self, control_channel): + # connect_fun (conn, addr) + asyncore.dispatcher.__init__ (self) + self.control_channel = control_channel + self.create_socket (socket.AF_INET, socket.SOCK_STREAM) + # bind to an address on the interface that the + # control connection is coming from. + self.bind (( + self.control_channel.getsockname()[0], + 0 + )) + self.addr = self.getsockname() + self.listen (1) + +# def __del__ (self): +# print 'passive_acceptor.__del__()' + + def log (self, *ignore): + pass + + def handle_accept (self): + conn, addr = self.accept() + dc = self.control_channel.client_dc + if dc is not None: + dc.set_socket (conn) + dc.addr = addr + dc.connected = 1 + self.control_channel.passive_acceptor = None + else: + self.ready = conn, addr + self.close() + + +class xmit_channel (asynchat.async_chat): + + # for an ethernet, you want this to be fairly large, in fact, it + # _must_ be large for performance comparable to an ftpd. [64k] we + # ought to investigate automatically-sized buffers... + + ac_out_buffer_size = 16384 + bytes_out = 0 + + def __init__ (self, channel, client_addr=None): + self.channel = channel + self.client_addr = client_addr + asynchat.async_chat.__init__ (self) + +# def __del__ (self): +# print 'xmit_channel.__del__()' + + def log (self, *args): + pass + + def readable (self): + return not self.connected + + def writable (self): + return 1 + + def send (self, data): + result = asynchat.async_chat.send (self, data) + self.bytes_out = self.bytes_out + result + return result + + def handle_error (self): + # usually this is to catch an unexpected disconnect. + self.log_info ('unexpected disconnect on data xmit channel', 'error') + try: + self.close() + except: + pass + + # TODO: there's a better way to do this. we need to be able to + # put 'events' in the producer fifo. to do this cleanly we need + # to reposition the 'producer' fifo as an 'event' fifo. + + def close (self): + c = self.channel + s = c.server + c.client_dc = None + s.total_files_out.increment() + s.total_bytes_out.increment (self.bytes_out) + if not len(self.producer_fifo): + c.respond ('226 Transfer complete') + elif not c.closed: + c.respond ('426 Connection closed; transfer aborted') + del c + del s + del self.channel + asynchat.async_chat.close (self) + +class recv_channel (asyncore.dispatcher): + def __init__ (self, channel, client_addr, fd): + self.channel = channel + self.client_addr = client_addr + self.fd = fd + asyncore.dispatcher.__init__ (self) + self.bytes_in = counter() + + def log (self, *ignore): + pass + + def handle_connect (self): + pass + + def writable (self): + return 0 + + def recv (*args): + result = apply (asyncore.dispatcher.recv, args) + self = args[0] + self.bytes_in.increment(len(result)) + return result + + buffer_size = 8192 + + def handle_read (self): + block = self.recv (self.buffer_size) + if block: + try: + self.fd.write (block) + except IOError: + self.log_info ('got exception writing block...', 'error') + + def handle_close (self): + s = self.channel.server + s.total_files_in.increment() + s.total_bytes_in.increment(self.bytes_in.as_long()) + self.fd.close() + self.channel.respond ('226 Transfer complete.') + self.close() + +import filesys + +# not much of a doorman! 8^) +class dummy_authorizer: + def __init__ (self, root='/'): + self.root = root + def authorize (self, channel, username, password): + channel.persona = -1, -1 + channel.read_only = 1 + return 1, 'Ok.', filesys.os_filesystem (self.root) + +class anon_authorizer: + def __init__ (self, root='/'): + self.root = root + + def authorize (self, channel, username, password): + if username in ('ftp', 'anonymous'): + channel.persona = -1, -1 + channel.read_only = 1 + return 1, 'Ok.', filesys.os_filesystem (self.root) + else: + return 0, 'Password invalid.', None + +# =========================================================================== +# Unix-specific improvements +# =========================================================================== + +if os.name == 'posix': + + class unix_authorizer: + # return a trio of (success, reply_string, filesystem) + def authorize (self, channel, username, password): + import crypt + import pwd + try: + info = pwd.getpwnam (username) + except KeyError: + return 0, 'No such user.', None + mangled = info[1] + if crypt.crypt (password, mangled[:2]) == mangled: + channel.read_only = 0 + fs = filesys.schizophrenic_unix_filesystem ( + '/', + info[5], + persona = (info[2], info[3]) + ) + return 1, 'Login successful.', fs + else: + return 0, 'Password invalid.', None + + def __repr__ (self): + return '<standard unix authorizer>' + + # simple anonymous ftp support + class unix_authorizer_with_anonymous (unix_authorizer): + def __init__ (self, root=None, real_users=0): + self.root = root + self.real_users = real_users + + def authorize (self, channel, username, password): + if string.lower(username) in ['anonymous', 'ftp']: + import pwd + try: + # ok, here we run into lots of confusion. + # on some os', anon runs under user 'nobody', + # on others as 'ftp'. ownership is also critical. + # need to investigate. + # linux: new linuxen seem to have nobody's UID=-1, + # which is an illegal value. Use ftp. + ftp_user_info = pwd.getpwnam ('ftp') + if string.lower(os.uname()[0]) == 'linux': + nobody_user_info = pwd.getpwnam ('ftp') + else: + nobody_user_info = pwd.getpwnam ('nobody') + channel.read_only = 1 + if self.root is None: + self.root = ftp_user_info[5] + fs = filesys.unix_filesystem (self.root, '/') + return 1, 'Anonymous Login Successful', fs + except KeyError: + return 0, 'Anonymous account not set up', None + elif self.real_users: + return unix_authorizer.authorize ( + self, + channel, + username, + password + ) + else: + return 0, 'User logins not allowed', None + +# usage: ftp_server /PATH/TO/FTP/ROOT PORT +# for example: +# $ ftp_server /home/users/ftp 8021 + +if os.name == 'posix': + def test (port='8021'): + fs = ftp_server ( + unix_authorizer(), + port=string.atoi (port) + ) + try: + asyncore.loop() + except KeyboardInterrupt: + fs.log_info('FTP server shutting down. (received SIGINT)', 'warning') + # close everything down on SIGINT. + # of course this should be a cleaner shutdown. + asyncore.close_all() + + if __name__ == '__main__': + test (sys.argv[1]) +# not unix +else: + def test (): + fs = ftp_server (dummy_authorizer()) + if __name__ == '__main__': + test () + +# this is the command list from the wuftpd man page +# '*' means we've implemented it. +# '!' requires write access +# +command_documentation = { + 'abor': 'abort previous command', #* + 'acct': 'specify account (ignored)', + 'allo': 'allocate storage (vacuously)', + 'appe': 'append to a file', #*! + 'cdup': 'change to parent of current working directory', #* + 'cwd': 'change working directory', #* + 'dele': 'delete a file', #! + 'help': 'give help information', #* + 'list': 'give list files in a directory', #* + 'mkd': 'make a directory', #! + 'mdtm': 'show last modification time of file', #* + 'mode': 'specify data transfer mode', + 'nlst': 'give name list of files in directory', #* + 'noop': 'do nothing', #* + 'pass': 'specify password', #* + 'pasv': 'prepare for server-to-server transfer', #* + 'port': 'specify data connection port', #* + 'pwd': 'print the current working directory', #* + 'quit': 'terminate session', #* + 'rest': 'restart incomplete transfer', #* + 'retr': 'retrieve a file', #* + 'rmd': 'remove a directory', #! + 'rnfr': 'specify rename-from file name', #! + 'rnto': 'specify rename-to file name', #! + 'site': 'non-standard commands (see next section)', + 'size': 'return size of file', #* + 'stat': 'return status of server', #* + 'stor': 'store a file', #*! + 'stou': 'store a file with a unique name', #! + 'stru': 'specify data transfer structure', + 'syst': 'show operating system type of server system', #* + 'type': 'specify data transfer type', #* + 'user': 'specify user name', #* + 'xcup': 'change to parent of current working directory (deprecated)', + 'xcwd': 'change working directory (deprecated)', + 'xmkd': 'make a directory (deprecated)', #! + 'xpwd': 'print the current working directory (deprecated)', + 'xrmd': 'remove a directory (deprecated)', #! +} + + +# debugging aid (linux) +def get_vm_size (): + return string.atoi (string.split(open ('/proc/self/stat').readline())[22]) + +def print_vm(): + print 'vm: %8dk' % (get_vm_size()/1024) diff --git a/demo/medusa054/ftps_server.py b/demo/medusa054/ftps_server.py new file mode 100644 index 0000000..bf2f5a9 --- /dev/null +++ b/demo/medusa054/ftps_server.py @@ -0,0 +1,438 @@ +"""An FTP/TLS server built on Medusa's ftp_server. + +Copyright (c) 1999-2004 Ng Pheng Siong. All rights reserved.""" + +# Python +import socket, string, sys, time + +# Medusa +from counter import counter +import asynchat, asyncore, ftp_server, logger + +# M2Crypto +from M2Crypto import SSL, version + +VERSION_STRING=version + +class ftp_tls_channel(ftp_server.ftp_channel): + + """FTP/TLS server channel for Medusa.""" + + def __init__(self, server, ssl_ctx, conn, addr): + """Initialise the channel.""" + self.ssl_ctx = ssl_ctx + self.server = server + self.current_mode = 'a' + self.addr = addr + asynchat.async_chat.__init__(self, conn) + self.set_terminator('\r\n') + self.client_addr = (addr[0], 21) + self.client_dc = None + self.in_buffer = '' + self.closing = 0 + self.passive_acceptor = None + self.passive_connection = None + self.filesystem = None + self.authorized = 0 + self._ssl_accepting = 0 + self._ssl_accepted = 0 + self._pbsz = None + self._prot = None + resp = '220 %s M2Crypto (Medusa) FTP/TLS server v%s ready.' + self.respond(resp % (self.server.hostname, VERSION_STRING)) + + def writable(self): + return self._ssl_accepting or self._ssl_accepted + + def handle_read(self): + """Handle a read event.""" + if self._ssl_accepting: + self._ssl_accepted = self.socket.accept_ssl() + if self._ssl_accepted: + self._ssl_accepting = 0 + else: + try: + ftp_server.ftp_channel.handle_read(self) + except SSL.SSLError, what: + if str(what) == 'unexpected eof': + self.close() + else: + raise + + def handle_write(self): + """Handle a write event.""" + if self._ssl_accepting: + self._ssl_accepted = self.socket.accept_ssl() + if self._ssl_accepted: + self._ssl_accepting = 0 + else: + try: + ftp_server.ftp_channel.handle_write(self) + except SSL.SSLError, what: + if str(what) == 'unexpected eof': + self.close() + else: + raise + + def send(self, data): + """Send data over SSL.""" + try: + result = self.socket.send(data) + if result <= 0: + return 0 + else: + return result + except SSL.SSLError, what: + self.close() + self.log_info('send: closing channel %s %s' % (repr(self), what)) + return 0 + + def recv(self, buffer_size): + """Receive data over SSL.""" + try: + result = self.socket.recv(buffer_size) + if not result: + return '' + else: + return result + except SSL.SSLError, what: + self.close() + self.log_info('recv: closing channel %s %s' % (repr(self), what)) + return '' + + def found_terminator(self): + """Dispatch the FTP command.""" + line = self.in_buffer + if not len(line): + return + + sp = string.find(line, ' ') + if sp != -1: + line = [line[:sp], line[sp+1:]] + else: + line = [line] + + command = string.lower(line[0]) + if string.find(command, 'stor') != -1: + while command and command[0] not in string.letters: + command = command[1:] + + func_name = 'cmd_%s' % command + if command != 'pass': + self.log('<== %s' % repr(self.in_buffer)[1:-1]) + else: + self.log('<== %s' % line[0]+' <password>') + + self.in_buffer = '' + if not hasattr(self, func_name): + self.command_not_understood(line[0]) + return + + func = getattr(self, func_name) + if not self.check_command_authorization(command): + self.command_not_authorized(command) + else: + try: + result = apply(func, (line,)) + except: + self.server.total_exceptions.increment() + (file, func, line), t, v, tbinfo = asyncore.compact_traceback() + if self.client_dc: + try: + self.client_dc_close() + except: + pass + resp = '451 Server error: %s, %s: file %s line: %s' + self.respond(resp % (t, v, file, line)) + + def make_xmit_channel(self): + """Create a connection for sending data.""" + pa = self.passive_acceptor + if pa: + if pa.ready: + conn, addr = pa.ready + if self._prot: + cdc = tls_xmit_channel(self, conn, self.ssl_ctx, addr) + else: + cdc = ftp_server.xmit_channel(self, addr) + cdc.set_socket(conn) + cdc.connected = 1 + self.passive_acceptor.close() + self.passive_acceptor = None + else: + if self._prot: + cdc = tls_xmit_channel(self, None, self.ssl_ctx, None) + else: + cdc = ftp_server.xmit_channel(self) + else: + if self._prot: + cdc = tls_xmit_channel(self, None, self.ssl_ctx, self.client_addr) + else: + cdc = ftp_server.xmit_channel(self, self.client_addr) + cdc.create_socket(socket.AF_INET, socket.SOCK_STREAM) + if self.bind_local_minus_one: + cdc.bind(('', self.server.port - 1)) + try: + cdc.connect(self.client_addr) + except socket.error, what: + self.respond('425 Cannot build data connection') + self.client_dc = cdc + + def make_recv_channel(self, fd): + """Create a connection for receiving data.""" + pa = self.passive_acceptor + if pa: + if pa.ready: + conn, addr = pa.ready + if self._prot: + cdc = tls_recv_channel(self, conn, self.ssl_ctx, addr, fd) + else: + cdc = ftp_server.recv_channel(self, addr, fd) + cdc.set_socket(conn) + cdc.connected = 1 + self.passive_acceptor.close() + self.passive_acceptor = None + else: + if self._prot: + cdc = tls_recv_channel(self, None, self.ssl_ctx, None, fd) + else: + cdc = ftp_server.recv_channel(self, None, fd) + else: + if self._prot: + cdc = tls_recv_channel(self, None, self.ssl_ctx, self._prot, self.client_addr, fd) + else: + cdc = ftp_server.recv_channel(self, self.client_addr, fd) + cdc.create_socket(socket.AF_INET, socket.SOCK_STREAM) + try: + cdc.connect(self.client_addr) + except socket.error, what: + self.respond('425 Cannot build data connection') + self.client_dc = cdc + + def cmd_auth(self, line): + """Prepare for TLS operation.""" + # XXX Handle variations. + if line[1] != 'TLS': + self.command_not_understood (string.join(line)) + else: + self.respond('234 AUTH TLS successful') + self._ssl_accepting = 1 + self.socket = SSL.Connection(self.ssl_ctx, self.socket) + self.socket.setup_addr(self.addr) + self.socket.setup_ssl() + self.socket.set_accept_state() + self._ssl_accepted = self.socket.accept_ssl() + if self._ssl_accepted: + self._ssl_accepting = 0 + + def cmd_pbsz(self, line): + """Negotiate size of buffer for secure data transfer. For + FTP/TLS the only valid value for the parameter is '0'; any + other value is accepted but ignored.""" + if not (self._ssl_accepting or self._ssl_accepted): + return self.respond('503 AUTH TLS must be issued prior to PBSZ') + self._pbsz = 1 + self.respond('200 PBSZ=0 successful.') + + def cmd_prot(self, line): + """Negotiate the security level of the data connection.""" + if self._pbsz is None: + return self.respond('503 PBSZ must be issued prior to PROT') + if line[1] == 'C': + self.respond('200 Protection set to Clear') + self._pbsz = None + self._prot = None + elif line[1] == 'P': + self.respond('200 Protection set to Private') + self._prot = 1 + elif line[1] in ('S', 'E'): + self.respond('536 PROT %s unsupported' % line[1]) + else: + self.respond('504 PROT %s unsupported' % line[1]) + + +class ftp_tls_server(ftp_server.ftp_server): + + """FTP/TLS server for Medusa.""" + + SERVER_IDENT = 'M2Crypto FTP/TLS Server (v%s)' % VERSION_STRING + + ftp_channel_class = ftp_tls_channel + + def __init__(self, authz, ssl_ctx, host=None, ip='', port=21, resolver=None, log_obj=None): + """Initialise the server.""" + self.ssl_ctx = ssl_ctx + self.ip = ip + self.port = port + self.authorizer = authz + + if host is None: + self.hostname = socket.gethostname() + else: + self.hostname = host + + self.total_sessions = counter() + self.closed_sessions = counter() + self.total_files_out = counter() + self.total_files_in = counter() + self.total_bytes_out = counter() + self.total_bytes_in = counter() + self.total_exceptions = counter() + + asyncore.dispatcher.__init__(self) + self.create_socket(socket.AF_INET, socket.SOCK_STREAM) + self.set_reuse_addr() + self.bind((self.ip, self.port)) + self.listen(5) + + if log_obj is None: + log_obj = sys.stdout + + if resolver: + self.logger = logger.resolving_logger(resolver, log_obj) + else: + self.logger = logger.unresolving_logger(logger.file_logger(sys.stdout)) + + l = 'M2Crypto (Medusa) FTP/TLS server started at %s\n\tAuthz: %s\n\tHostname: %s\n\tPort: %d' + self.log_info(l % (time.ctime(time.time()), repr(self.authorizer), self.hostname, self.port)) + + def handle_accept(self): + """Accept a socket and dispatch a channel to handle it.""" + conn, addr = self.accept() + self.total_sessions.increment() + self.log_info('Connection from %s:%d' % addr) + self.ftp_channel_class(self, self.ssl_ctx, conn, addr) + + +class nbio_ftp_tls_actor: + + """TLS protocol negotiation mixin for FTP/TLS.""" + + def tls_init(self, sock, ssl_ctx, client_addr): + """Perform TLS protocol negotiation.""" + self.ssl_ctx = ssl_ctx + self.client_addr = client_addr + self._ssl_handshaking = 1 + self._ssl_handshake_ok = 0 + if sock: + self.socket = SSL.Connection(self.ssl_ctx, sock) + self.socket.setup_addr(self.client_addr) + self.socket.setup_ssl() + self._ssl_handshake_ok = self.socket.accept_ssl() + if self._ssl_handshake_ok: + self._ssl_handshaking = 0 + self.add_channel() + # else the client hasn't connected yet; when that happens, + # handle_connect() will be triggered. + + def tls_neg_ok(self): + """Return status of TLS protocol negotiation.""" + if self._ssl_handshaking: + self._ssl_handshake_ok = self.socket.accept_ssl() + if self._ssl_handshake_ok: + self._ssl_handshaking = 0 + return self._ssl_handshake_ok + + def handle_connect(self): + """Handle a data connection that occurs after this instance came + into being. When this handler is triggered, self.socket has been + created and refers to the underlying connected socket.""" + self.socket = SSL.Connection(self.ssl_ctx, self.socket) + self.socket.setup_addr(self.client_addr) + self.socket.setup_ssl() + self._ssl_handshake_ok = self.socket.accept_ssl() + if self._ssl_handshake_ok: + self._ssl_handshaking = 0 + self.add_channel() + + def send(self, data): + """Send data over SSL.""" + try: + result = self.socket.send(data) + if result <= 0: + return 0 + else: + return result + except SSL.SSLError, what: + self.close() + self.log_info('send: closing channel %s %s' % (repr(self), what)) + return 0 + + def recv(self, buffer_size): + """Receive data over SSL.""" + try: + result = self.socket.recv(buffer_size) + if not result: + return '' + else: + return result + except SSL.SSLError, what: + self.close() + self.log_info('recv: closing channel %s %s' % (repr(self), what)) + return '' + + +class tls_xmit_channel(nbio_ftp_tls_actor, ftp_server.xmit_channel): + + """TLS driver for a send-only data connection.""" + + def __init__(self, channel, conn, ssl_ctx, client_addr=None): + """Initialise the driver.""" + ftp_server.xmit_channel.__init__(self, channel, client_addr) + self.tls_init(conn, ssl_ctx, client_addr) + + def readable(self): + """This channel is readable iff TLS negotiation is in progress. + (Which implies a connected channel, of course.)""" + if not self.connected: + return 0 + else: + return self._ssl_handshaking + + def writable(self): + """This channel is writable iff TLS negotiation is in progress + or the application has data to send.""" + if self._ssl_handshaking: + return 1 + else: + return ftp_server.xmit_channel.writable(self) + + def handle_read(self): + """Handle a read event: either continue with TLS negotiation + or let the application handle this event.""" + if self.tls_neg_ok(): + ftp_server.xmit_channel.handle_read(self) + + def handle_write(self): + """Handle a write event: either continue with TLS negotiation + or let the application handle this event.""" + if self.tls_neg_ok(): + ftp_server.xmit_channel.handle_write(self) + + +class tls_recv_channel(nbio_ftp_tls_actor, ftp_server.recv_channel): + + """TLS driver for a receive-only data connection.""" + + def __init__(self, channel, conn, ssl_ctx, client_addr, fd): + """Initialise the driver.""" + ftp_server.recv_channel.__init__(self, channel, client_addr, fd) + self.tls_init(conn, ssl_ctx, client_addr) + + def writable(self): + """This channel is writable iff TLS negotiation is in progress.""" + return self._ssl_handshaking + + def handle_read(self): + """Handle a read event: either continue with TLS negotiation + or let the application handle this event.""" + if self.tls_neg_ok(): + ftp_server.recv_channel.handle_read(self) + + def handle_write(self): + """Handle a write event: either continue with TLS negotiation + or let the application handle this event.""" + if self.tls_neg_ok(): + ftp_server.recv_channel.handle_write(self) + + diff --git a/demo/medusa054/http_date.py b/demo/medusa054/http_date.py new file mode 100644 index 0000000..d2f90b8 --- /dev/null +++ b/demo/medusa054/http_date.py @@ -0,0 +1,126 @@ +# -*- Mode: Python -*- + +import re +import string +import time + +def concat (*args): + return ''.join (args) + +def join (seq, field=' '): + return field.join (seq) + +def group (s): + return '(' + s + ')' + +short_days = ['sun','mon','tue','wed','thu','fri','sat'] +long_days = ['sunday','monday','tuesday','wednesday','thursday','friday','saturday'] + +short_day_reg = group (join (short_days, '|')) +long_day_reg = group (join (long_days, '|')) + +daymap = {} +for i in range(7): + daymap[short_days[i]] = i + daymap[long_days[i]] = i + +hms_reg = join (3 * [group('[0-9][0-9]')], ':') + +months = ['jan','feb','mar','apr','may','jun','jul','aug','sep','oct','nov','dec'] + +monmap = {} +for i in range(12): + monmap[months[i]] = i+1 + +months_reg = group (join (months, '|')) + +# From draft-ietf-http-v11-spec-07.txt/3.3.1 +# Sun, 06 Nov 1994 08:49:37 GMT ; RFC 822, updated by RFC 1123 +# Sunday, 06-Nov-94 08:49:37 GMT ; RFC 850, obsoleted by RFC 1036 +# Sun Nov 6 08:49:37 1994 ; ANSI C's asctime() format + +# rfc822 format +rfc822_date = join ( + [concat (short_day_reg,','), # day + group('[0-9][0-9]?'), # date + months_reg, # month + group('[0-9]+'), # year + hms_reg, # hour minute second + 'gmt' + ], + ' ' + ) + +rfc822_reg = re.compile (rfc822_date) + +def unpack_rfc822 (m): + g = m.group + a = string.atoi + return ( + a(g(4)), # year + monmap[g(3)], # month + a(g(2)), # day + a(g(5)), # hour + a(g(6)), # minute + a(g(7)), # second + 0, + 0, + 0 + ) + +# rfc850 format +rfc850_date = join ( + [concat (long_day_reg,','), + join ( + [group ('[0-9][0-9]?'), + months_reg, + group ('[0-9]+') + ], + '-' + ), + hms_reg, + 'gmt' + ], + ' ' + ) + +rfc850_reg = re.compile (rfc850_date) +# they actually unpack the same way +def unpack_rfc850 (m): + g = m.group + a = string.atoi + return ( + a(g(4)), # year + monmap[g(3)], # month + a(g(2)), # day + a(g(5)), # hour + a(g(6)), # minute + a(g(7)), # second + 0, + 0, + 0 + ) + +# parsdate.parsedate - ~700/sec. +# parse_http_date - ~1333/sec. + +def build_http_date (when): + return time.strftime ('%a, %d %b %Y %H:%M:%S GMT', time.gmtime(when)) + +def parse_http_date (d): + d = string.lower (d) + tz = time.timezone + m = rfc850_reg.match (d) + if m and m.end() == len(d): + retval = int (time.mktime (unpack_rfc850(m)) - tz) + else: + m = rfc822_reg.match (d) + if m and m.end() == len(d): + retval = int (time.mktime (unpack_rfc822(m)) - tz) + else: + return 0 + # Thanks to Craig Silverstein <csilvers@google.com> for pointing + # out the DST discrepancy + if time.daylight and time.localtime(retval)[-1] == 1: # DST correction + retval = retval + (tz - time.altzone) + return retval diff --git a/demo/medusa054/http_server.py b/demo/medusa054/http_server.py new file mode 100644 index 0000000..8502add --- /dev/null +++ b/demo/medusa054/http_server.py @@ -0,0 +1,749 @@ +#! /usr/local/bin/python +# -*- Mode: Python -*- +# +# Author: Sam Rushing <rushing@nightmare.com> +# Copyright 1996-2000 by Sam Rushing +# All Rights Reserved. +# + +# python modules +import os +import re +import socket +import string +import sys +import time + +# async modules +import asyncore +import asynchat + +# medusa modules +import http_date +import producers +import status_handler +import logger + +VERSION_STRING = '1.1' + +from counter import counter +from urllib import unquote, splitquery + +# =========================================================================== +# Request Object +# =========================================================================== + +class http_request: + + # default reply code + reply_code = 200 + + request_counter = counter() + + # Whether to automatically use chunked encoding when + # + # HTTP version is 1.1 + # Content-Length is not set + # Chunked encoding is not already in effect + # + # If your clients are having trouble, you might want to disable this. + use_chunked = 1 + + # by default, this request object ignores user data. + collector = None + + def __init__ (self, *args): + # unpack information about the request + (self.channel, self.request, + self.command, self.uri, self.version, + self.header) = args + + self.outgoing = [] + self.reply_headers = { + 'Server' : 'Medusa/%s' % VERSION_STRING, + 'Date' : http_date.build_http_date (time.time()) + } + self.request_number = http_request.request_counter.increment() + self._split_uri = None + self._header_cache = {} + + # -------------------------------------------------- + # reply header management + # -------------------------------------------------- + def __setitem__ (self, key, value): + self.reply_headers[key] = value + + def __getitem__ (self, key): + return self.reply_headers[key] + + def has_key (self, key): + return self.reply_headers.has_key (key) + + def build_reply_header (self): + return string.join ( + [self.response(self.reply_code)] + map ( + lambda x: '%s: %s' % x, + self.reply_headers.items() + ), + '\r\n' + ) + '\r\n\r\n' + + # -------------------------------------------------- + # split a uri + # -------------------------------------------------- + + # <path>;<params>?<query>#<fragment> + path_regex = re.compile ( + # path params query fragment + r'([^;?#]*)(;[^?#]*)?(\?[^#]*)?(#.*)?' + ) + + def split_uri (self): + if self._split_uri is None: + m = self.path_regex.match (self.uri) + if m.end() != len(self.uri): + raise ValueError, "Broken URI" + else: + self._split_uri = m.groups() + return self._split_uri + + def get_header_with_regex (self, head_reg, group): + for line in self.header: + m = head_reg.match (line) + if m.end() == len(line): + return m.group (group) + return '' + + def get_header (self, header): + header = string.lower (header) + hc = self._header_cache + if not hc.has_key (header): + h = header + ': ' + hl = len(h) + for line in self.header: + if string.lower (line[:hl]) == h: + r = line[hl:] + hc[header] = r + return r + hc[header] = None + return None + else: + return hc[header] + + # -------------------------------------------------- + # user data + # -------------------------------------------------- + + def collect_incoming_data (self, data): + if self.collector: + self.collector.collect_incoming_data (data) + else: + self.log_info( + 'Dropping %d bytes of incoming request data' % len(data), + 'warning' + ) + + def found_terminator (self): + if self.collector: + self.collector.found_terminator() + else: + self.log_info ( + 'Unexpected end-of-record for incoming request', + 'warning' + ) + + def push (self, thing): + if type(thing) == type(''): + self.outgoing.append(producers.simple_producer (thing)) + else: + self.outgoing.append(thing) + + def response (self, code=200): + message = self.responses[code] + self.reply_code = code + return 'HTTP/%s %d %s' % (self.version, code, message) + + def error (self, code): + self.reply_code = code + message = self.responses[code] + s = self.DEFAULT_ERROR_MESSAGE % { + 'code': code, + 'message': message, + } + self['Content-Length'] = len(s) + self['Content-Type'] = 'text/html' + # make an error reply + self.push (s) + self.done() + + # can also be used for empty replies + reply_now = error + + def done (self): + "finalize this transaction - send output to the http channel" + + # ---------------------------------------- + # persistent connection management + # ---------------------------------------- + + # --- BUCKLE UP! ---- + + connection = string.lower (get_header (CONNECTION, self.header)) + + close_it = 0 + wrap_in_chunking = 0 + + if self.version == '1.0': + if connection == 'keep-alive': + if not self.has_key ('Content-Length'): + close_it = 1 + else: + self['Connection'] = 'Keep-Alive' + else: + close_it = 1 + elif self.version == '1.1': + if connection == 'close': + close_it = 1 + elif not self.has_key ('Content-Length'): + if self.has_key ('Transfer-Encoding'): + if not self['Transfer-Encoding'] == 'chunked': + close_it = 1 + elif self.use_chunked: + self['Transfer-Encoding'] = 'chunked' + wrap_in_chunking = 1 + else: + close_it = 1 + elif self.version is None: + # Although we don't *really* support http/0.9 (because we'd have to + # use \r\n as a terminator, and it would just yuck up a lot of stuff) + # it's very common for developers to not want to type a version number + # when using telnet to debug a server. + close_it = 1 + + outgoing_header = producers.simple_producer (self.build_reply_header()) + + if close_it: + self['Connection'] = 'close' + + if wrap_in_chunking: + outgoing_producer = producers.chunked_producer ( + producers.composite_producer (self.outgoing) + ) + # prepend the header + outgoing_producer = producers.composite_producer( + [outgoing_header, outgoing_producer] + ) + else: + # prepend the header + self.outgoing.insert(0, outgoing_header) + outgoing_producer = producers.composite_producer (self.outgoing) + + # apply a few final transformations to the output + self.channel.push_with_producer ( + # globbing gives us large packets + producers.globbing_producer ( + # hooking lets us log the number of bytes sent + producers.hooked_producer ( + outgoing_producer, + self.log + ) + ) + ) + + self.channel.current_request = None + + if close_it: + self.channel.close_when_done() + + def log_date_string (self, when): + gmt = time.gmtime(when) + if time.daylight and gmt[8]: + tz = time.altzone + else: + tz = time.timezone + if tz > 0: + neg = 1 + else: + neg = 0 + tz = -tz + h, rem = divmod (tz, 3600) + m, rem = divmod (rem, 60) + if neg: + offset = '-%02d%02d' % (h, m) + else: + offset = '+%02d%02d' % (h, m) + + return time.strftime ( '%d/%b/%Y:%H:%M:%S ', gmt) + offset + + def log (self, bytes): + self.channel.server.logger.log ( + self.channel.addr[0], + '%d - - [%s] "%s" %d %d\n' % ( + self.channel.addr[1], + self.log_date_string (time.time()), + self.request, + self.reply_code, + bytes + ) + ) + + responses = { + 100: "Continue", + 101: "Switching Protocols", + 200: "OK", + 201: "Created", + 202: "Accepted", + 203: "Non-Authoritative Information", + 204: "No Content", + 205: "Reset Content", + 206: "Partial Content", + 300: "Multiple Choices", + 301: "Moved Permanently", + 302: "Moved Temporarily", + 303: "See Other", + 304: "Not Modified", + 305: "Use Proxy", + 400: "Bad Request", + 401: "Unauthorized", + 402: "Payment Required", + 403: "Forbidden", + 404: "Not Found", + 405: "Method Not Allowed", + 406: "Not Acceptable", + 407: "Proxy Authentication Required", + 408: "Request Time-out", + 409: "Conflict", + 410: "Gone", + 411: "Length Required", + 412: "Precondition Failed", + 413: "Request Entity Too Large", + 414: "Request-URI Too Large", + 415: "Unsupported Media Type", + 500: "Internal Server Error", + 501: "Not Implemented", + 502: "Bad Gateway", + 503: "Service Unavailable", + 504: "Gateway Time-out", + 505: "HTTP Version not supported" + } + + # Default error message + DEFAULT_ERROR_MESSAGE = string.join ( + ['<head>', + '<title>Error response</title>', + '</head>', + '<body>', + '<h1>Error response</h1>', + '<p>Error code %(code)d.', + '<p>Message: %(message)s.', + '</body>', + '' + ], + '\r\n' + ) + + +# =========================================================================== +# HTTP Channel Object +# =========================================================================== + +class http_channel (asynchat.async_chat): + + # use a larger default output buffer + ac_out_buffer_size = 1<<16 + + current_request = None + channel_counter = counter() + + def __init__ (self, server, conn, addr): + self.channel_number = http_channel.channel_counter.increment() + self.request_counter = counter() + asynchat.async_chat.__init__ (self, conn) + self.server = server + self.addr = addr + self.set_terminator ('\r\n\r\n') + self.in_buffer = '' + self.creation_time = int (time.time()) + self.check_maintenance() + + def __repr__ (self): + ar = asynchat.async_chat.__repr__(self)[1:-1] + return '<%s channel#: %s requests:%s>' % ( + ar, + self.channel_number, + self.request_counter + ) + + # Channel Counter, Maintenance Interval... + maintenance_interval = 500 + + def check_maintenance (self): + if not self.channel_number % self.maintenance_interval: + self.maintenance() + + def maintenance (self): + self.kill_zombies() + + # 30-minute zombie timeout. status_handler also knows how to kill zombies. + zombie_timeout = 30 * 60 + + def kill_zombies (self): + now = int (time.time()) + for channel in asyncore.socket_map.values(): + if channel.__class__ == self.__class__: + if (now - channel.creation_time) > channel.zombie_timeout: + channel.close() + + # -------------------------------------------------- + # send/recv overrides, good place for instrumentation. + # -------------------------------------------------- + + # this information needs to get into the request object, + # so that it may log correctly. + def send (self, data): + result = asynchat.async_chat.send (self, data) + self.server.bytes_out.increment (len(data)) + return result + + def recv (self, buffer_size): + try: + result = asynchat.async_chat.recv (self, buffer_size) + self.server.bytes_in.increment (len(result)) + return result + except MemoryError: + # --- Save a Trip to Your Service Provider --- + # It's possible for a process to eat up all the memory of + # the machine, and put it in an extremely wedged state, + # where medusa keeps running and can't be shut down. This + # is where MemoryError tends to get thrown, though of + # course it could get thrown elsewhere. + sys.exit ("Out of Memory!") + + def handle_error (self): + t, v = sys.exc_info()[:2] + if t is SystemExit: + raise t, v + else: + asynchat.async_chat.handle_error (self) + + def log (self, *args): + pass + + # -------------------------------------------------- + # async_chat methods + # -------------------------------------------------- + + def collect_incoming_data (self, data): + if self.current_request: + # we are receiving data (probably POST data) for a request + self.current_request.collect_incoming_data (data) + else: + # we are receiving header (request) data + self.in_buffer = self.in_buffer + data + + def found_terminator (self): + if self.current_request: + self.current_request.found_terminator() + else: + header = self.in_buffer + self.in_buffer = '' + lines = string.split (header, '\r\n') + + # -------------------------------------------------- + # crack the request header + # -------------------------------------------------- + + while lines and not lines[0]: + # as per the suggestion of http-1.1 section 4.1, (and + # Eric Parker <eparker@zyvex.com>), ignore a leading + # blank lines (buggy browsers tack it onto the end of + # POST requests) + lines = lines[1:] + + if not lines: + self.close_when_done() + return + + request = lines[0] + + command, uri, version = crack_request (request) + header = join_headers (lines[1:]) + + # unquote path if necessary (thanks to Skip Montanaro for pointing + # out that we must unquote in piecemeal fashion). + rpath, rquery = splitquery(uri) + if '%' in rpath: + if rquery: + uri = unquote (rpath) + '?' + rquery + else: + uri = unquote (rpath) + + r = http_request (self, request, command, uri, version, header) + self.request_counter.increment() + self.server.total_requests.increment() + + if command is None: + self.log_info ('Bad HTTP request: %s' % repr(request), 'error') + r.error (400) + return + + # -------------------------------------------------- + # handler selection and dispatch + # -------------------------------------------------- + for h in self.server.handlers: + if h.match (r): + try: + self.current_request = r + # This isn't used anywhere. + # r.handler = h # CYCLE + h.handle_request (r) + except: + self.server.exceptions.increment() + (file, fun, line), t, v, tbinfo = asyncore.compact_traceback() + self.log_info( + 'Server Error: %s, %s: file: %s line: %s' % (t,v,file,line), + 'error') + try: + r.error (500) + except: + pass + return + + # no handlers, so complain + r.error (404) + + def writable_for_proxy (self): + # this version of writable supports the idea of a 'stalled' producer + # [i.e., it's not ready to produce any output yet] This is needed by + # the proxy, which will be waiting for the magic combination of + # 1) hostname resolved + # 2) connection made + # 3) data available. + if self.ac_out_buffer: + return 1 + elif len(self.producer_fifo): + p = self.producer_fifo.first() + if hasattr (p, 'stalled'): + return not p.stalled() + else: + return 1 + +# =========================================================================== +# HTTP Server Object +# =========================================================================== + +class http_server (asyncore.dispatcher): + + SERVER_IDENT = 'HTTP Server (V%s)' % VERSION_STRING + + channel_class = http_channel + + def __init__ (self, ip, port, resolver=None, logger_object=None): + self.ip = ip + self.port = port + asyncore.dispatcher.__init__ (self) + self.create_socket (socket.AF_INET, socket.SOCK_STREAM) + + self.handlers = [] + + if not logger_object: + logger_object = logger.file_logger (sys.stdout) + + self.set_reuse_addr() + self.bind ((ip, port)) + + # lower this to 5 if your OS complains + self.listen (1024) + + host, port = self.socket.getsockname() + if not ip: + self.log_info('Computing default hostname', 'warning') + ip = socket.gethostbyname (socket.gethostname()) + try: + self.server_name = socket.gethostbyaddr (ip)[0] + except socket.error: + self.log_info('Cannot do reverse lookup', 'warning') + self.server_name = ip # use the IP address as the "hostname" + + self.server_port = port + self.total_clients = counter() + self.total_requests = counter() + self.exceptions = counter() + self.bytes_out = counter() + self.bytes_in = counter() + + if not logger_object: + logger_object = logger.file_logger (sys.stdout) + + if resolver: + self.logger = logger.resolving_logger (resolver, logger_object) + else: + self.logger = logger.unresolving_logger (logger_object) + + self.log_info ( + 'Medusa (V%s) started at %s' + '\n\tHostname: %s' + '\n\tPort:%d' + '\n' % ( + VERSION_STRING, + time.ctime(time.time()), + self.server_name, + port, + ) + ) + + def writable (self): + return 0 + + def handle_read (self): + pass + + def readable (self): + return self.accepting + + def handle_connect (self): + pass + + def handle_accept (self): + self.total_clients.increment() + try: + conn, addr = self.accept() + except socket.error: + # linux: on rare occasions we get a bogus socket back from + # accept. socketmodule.c:makesockaddr complains that the + # address family is unknown. We don't want the whole server + # to shut down because of this. + self.log_info ('warning: server accept() threw an exception', 'warning') + return + except TypeError: + # unpack non-sequence. this can happen when a read event + # fires on a listening socket, but when we call accept() + # we get EWOULDBLOCK, so dispatcher.accept() returns None. + # Seen on FreeBSD3. + self.log_info ('warning: server accept() threw EWOULDBLOCK', 'warning') + return + + self.channel_class (self, conn, addr) + + def install_handler (self, handler, back=0): + if back: + self.handlers.append (handler) + else: + self.handlers.insert (0, handler) + + def remove_handler (self, handler): + self.handlers.remove (handler) + + def status (self): + def nice_bytes (n): + return string.join (status_handler.english_bytes (n)) + + handler_stats = filter (None, map (maybe_status, self.handlers)) + + if self.total_clients: + ratio = self.total_requests.as_long() / float(self.total_clients.as_long()) + else: + ratio = 0.0 + + return producers.composite_producer ( + [producers.lines_producer ( + ['<h2>%s</h2>' % self.SERVER_IDENT, + '<br>Listening on: <b>Host:</b> %s' % self.server_name, + '<b>Port:</b> %d' % self.port, + '<p><ul>' + '<li>Total <b>Clients:</b> %s' % self.total_clients, + '<b>Requests:</b> %s' % self.total_requests, + '<b>Requests/Client:</b> %.1f' % (ratio), + '<li>Total <b>Bytes In:</b> %s' % (nice_bytes (self.bytes_in.as_long())), + '<b>Bytes Out:</b> %s' % (nice_bytes (self.bytes_out.as_long())), + '<li>Total <b>Exceptions:</b> %s' % self.exceptions, + '</ul><p>' + '<b>Extension List</b><ul>', + ])] + handler_stats + [producers.simple_producer('</ul>')] + ) + +def maybe_status (thing): + if hasattr (thing, 'status'): + return thing.status() + else: + return None + +CONNECTION = re.compile ('Connection: (.*)', re.IGNORECASE) + +# merge multi-line headers +# [486dx2: ~500/sec] +def join_headers (headers): + r = [] + for i in range(len(headers)): + if headers[i][0] in ' \t': + r[-1] = r[-1] + headers[i][1:] + else: + r.append (headers[i]) + return r + +def get_header (head_reg, lines, group=1): + for line in lines: + m = head_reg.match (line) + if m and m.end() == len(line): + return m.group (group) + return '' + +def get_header_match (head_reg, lines): + for line in lines: + m = head_reg.match (line) + if m and m.end() == len(line): + return m + return '' + +REQUEST = re.compile ('([^ ]+) ([^ ]+)(( HTTP/([0-9.]+))$|$)') + +def crack_request (r): + m = REQUEST.match (r) + if m and m.end() == len(r): + if m.group(3): + version = m.group(5) + else: + version = None + return m.group(1), m.group(2), version + else: + return None, None, None + +if __name__ == '__main__': + import sys + if len(sys.argv) < 2: + print 'usage: %s <root> <port>' % (sys.argv[0]) + else: + import monitor + import filesys + import default_handler + import status_handler + import ftp_server + import chat_server + import resolver + import logger + rs = resolver.caching_resolver ('127.0.0.1') + lg = logger.file_logger (sys.stdout) + ms = monitor.secure_monitor_server ('fnord', '127.0.0.1', 9999) + fs = filesys.os_filesystem (sys.argv[1]) + dh = default_handler.default_handler (fs) + hs = http_server ('', string.atoi (sys.argv[2]), rs, lg) + hs.install_handler (dh) + ftp = ftp_server.ftp_server ( + ftp_server.dummy_authorizer(sys.argv[1]), + port=8021, + resolver=rs, + logger_object=lg + ) + cs = chat_server.chat_server ('', 7777) + sh = status_handler.status_extension([hs,ms,ftp,cs,rs]) + hs.install_handler (sh) + if ('-p' in sys.argv): + def profile_loop (): + try: + asyncore.loop() + except KeyboardInterrupt: + pass + import profile + profile.run ('profile_loop()', 'profile.out') + else: + asyncore.loop() diff --git a/demo/medusa054/https_server.py b/demo/medusa054/https_server.py new file mode 100644 index 0000000..f30bc86 --- /dev/null +++ b/demo/medusa054/https_server.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python + +"""A https server built on Medusa's http_server. + +Copyright (c) 1999-2004 Ng Pheng Siong. All rights reserved.""" + +import asynchat, asyncore, http_server, socket, sys +from M2Crypto import SSL, version + +VERSION_STRING=version + +class https_channel(http_server.http_channel): + + ac_in_buffer_size = 1 << 16 + + def __init__(self, server, conn, addr): + http_server.http_channel.__init__(self, server, conn, addr) + + def send(self, data): + try: + result = self.socket._write_nbio(data) + if result <= 0: + return 0 + else: + self.server.bytes_out.increment(result) + return result + except SSL.SSLError, why: + self.close() + self.log_info('send: closing channel %s %s' % (repr(self), why)) + return 0 + + def recv(self, buffer_size): + try: + result = self.socket._read_nbio(buffer_size) + if result is None: + return '' + elif result == '': + self.close() + return '' + else: + self.server.bytes_in.increment(len(result)) + return result + except SSL.SSLError, why: + self.close() + self.log_info('recv: closing channel %s %s' % (repr(self), why)) + return '' + + +class https_server(http_server.http_server): + + SERVER_IDENT='M2Crypto HTTPS Server (v%s)' % VERSION_STRING + + channel_class=https_channel + + def __init__(self, ip, port, ssl_ctx, resolver=None, logger_object=None): + http_server.http_server.__init__(self, ip, port, resolver, logger_object) + sys.stdout.write(self.SERVER_IDENT + '\n\n') + sys.stdout.flush() + self.ssl_ctx=ssl_ctx + + def handle_accept(self): + # Cribbed from http_server. + self.total_clients.increment() + try: + conn, addr = self.accept() + except socket.error: + # linux: on rare occasions we get a bogus socket back from + # accept. socketmodule.c:makesockaddr complains that the + # address family is unknown. We don't want the whole server + # to shut down because of this. + sys.stderr.write ('warning: server accept() threw an exception\n') + return + + # Turn the vanilla socket into an SSL connection. + try: + ssl_conn=SSL.Connection(self.ssl_ctx, conn) + ssl_conn._setup_ssl(addr) + ssl_conn.accept_ssl() + self.channel_class(self, ssl_conn, addr) + except SSL.SSLError: + pass + + def writeable(self): + return 0 + diff --git a/demo/medusa054/index.html b/demo/medusa054/index.html new file mode 100644 index 0000000..0f7de19 --- /dev/null +++ b/demo/medusa054/index.html @@ -0,0 +1,6 @@ +<html> +<title>M2Crypto HTTPS Server</title> +<body> +<h1>M2Crypto HTTPS Server - It works!</h1> +</body> +</html> diff --git a/demo/medusa054/logger.py b/demo/medusa054/logger.py new file mode 100644 index 0000000..d131ab5 --- /dev/null +++ b/demo/medusa054/logger.py @@ -0,0 +1,261 @@ +# -*- Mode: Python -*- + +import asynchat +import socket +import time # these three are for the rotating logger +import os # | +import stat # v + +# +# three types of log: +# 1) file +# with optional flushing. Also, one that rotates the log. +# 2) socket +# dump output directly to a socket connection. [how do we +# keep it open?] +# 3) syslog +# log to syslog via tcp. this is a per-line protocol. +# + +# +# The 'standard' interface to a logging object is simply +# log_object.log (message) +# + +# a file-like object that captures output, and +# makes sure to flush it always... this could +# be connected to: +# o stdio file +# o low-level file +# o socket channel +# o syslog output... + +class file_logger: + + # pass this either a path or a file object. + def __init__ (self, file, flush=1, mode='a'): + if type(file) == type(''): + if (file == '-'): + import sys + self.file = sys.stdout + else: + self.file = open (file, mode) + else: + self.file = file + self.do_flush = flush + + def __repr__ (self): + return '<file logger: %s>' % self.file + + def write (self, data): + self.file.write (data) + self.maybe_flush() + + def writeline (self, line): + self.file.writeline (line) + self.maybe_flush() + + def writelines (self, lines): + self.file.writelines (lines) + self.maybe_flush() + + def maybe_flush (self): + if self.do_flush: + self.file.flush() + + def flush (self): + self.file.flush() + + def softspace (self, *args): + pass + + def log (self, message): + if message[-1] not in ('\r', '\n'): + self.write (message + '\n') + else: + self.write (message) + +# like a file_logger, but it must be attached to a filename. +# When the log gets too full, or a certain time has passed, +# it backs up the log and starts a new one. Note that backing +# up the log is done via "mv" because anything else (cp, gzip) +# would take time, during which medusa would do nothing else. + +class rotating_file_logger (file_logger): + + # If freq is non-None we back up "daily", "weekly", or "monthly". + # Else if maxsize is non-None we back up whenever the log gets + # to big. If both are None we never back up. + def __init__ (self, file, freq=None, maxsize=None, flush=1, mode='a'): + self.filename = file + self.mode = mode + self.file = open (file, mode) + self.freq = freq + self.maxsize = maxsize + self.rotate_when = self.next_backup(self.freq) + self.do_flush = flush + + def __repr__ (self): + return '<rotating-file logger: %s>' % self.file + + # We back up at midnight every 1) day, 2) monday, or 3) 1st of month + def next_backup (self, freq): + (yr, mo, day, hr, min, sec, wd, jday, dst) = time.localtime(time.time()) + if freq == 'daily': + return time.mktime((yr,mo,day+1, 0,0,0, 0,0,-1)) + elif freq == 'weekly': + return time.mktime((yr,mo,day-wd+7, 0,0,0, 0,0,-1)) # wd(monday)==0 + elif freq == 'monthly': + return time.mktime((yr,mo+1,1, 0,0,0, 0,0,-1)) + else: + return None # not a date-based backup + + def maybe_flush (self): # rotate first if necessary + self.maybe_rotate() + if self.do_flush: # from file_logger() + self.file.flush() + + def maybe_rotate (self): + if self.freq and time.time() > self.rotate_when: + self.rotate() + self.rotate_when = self.next_backup(self.freq) + elif self.maxsize: # rotate when we get too big + try: + if os.stat(self.filename)[stat.ST_SIZE] > self.maxsize: + self.rotate() + except os.error: # file not found, probably + self.rotate() # will create a new file + + def rotate (self): + (yr, mo, day, hr, min, sec, wd, jday, dst) = time.localtime(time.time()) + try: + self.file.close() + newname = '%s.ends%04d%02d%02d' % (self.filename, yr, mo, day) + try: + open(newname, "r").close() # check if file exists + newname = newname + "-%02d%02d%02d" % (hr, min, sec) + except: # YEARMODY is unique + pass + os.rename(self.filename, newname) + self.file = open(self.filename, self.mode) + except: + pass + +# syslog is a line-oriented log protocol - this class would be +# appropriate for FTP or HTTP logs, but not for dumping stderr to. + +# TODO: a simple safety wrapper that will ensure that the line sent +# to syslog is reasonable. + +# TODO: async version of syslog_client: now, log entries use blocking +# send() + +import m_syslog +syslog_logger = m_syslog.syslog_client + +class syslog_logger (m_syslog.syslog_client): + def __init__ (self, address, facility='user'): + m_syslog.syslog_client.__init__ (self, address) + self.facility = m_syslog.facility_names[facility] + self.address=address + + def __repr__ (self): + return '<syslog logger address=%s>' % (repr(self.address)) + + def log (self, message): + m_syslog.syslog_client.log ( + self, + message, + facility=self.facility, + priority=m_syslog.LOG_INFO + ) + +# log to a stream socket, asynchronously + +class socket_logger (asynchat.async_chat): + + def __init__ (self, address): + + if type(address) == type(''): + self.create_socket (socket.AF_UNIX, socket.SOCK_STREAM) + else: + self.create_socket (socket.AF_INET, socket.SOCK_STREAM) + + self.connect (address) + self.address = address + + def __repr__ (self): + return '<socket logger: address=%s>' % (self.address) + + def log (self, message): + if message[-2:] != '\r\n': + self.socket.push (message + '\r\n') + else: + self.socket.push (message) + +# log to multiple places +class multi_logger: + def __init__ (self, loggers): + self.loggers = loggers + + def __repr__ (self): + return '<multi logger: %s>' % (repr(self.loggers)) + + def log (self, message): + for logger in self.loggers: + logger.log (message) + +class resolving_logger: + """Feed (ip, message) combinations into this logger to get a + resolved hostname in front of the message. The message will not + be logged until the PTR request finishes (or fails).""" + + def __init__ (self, resolver, logger): + self.resolver = resolver + self.logger = logger + + class logger_thunk: + def __init__ (self, message, logger): + self.message = message + self.logger = logger + + def __call__ (self, host, ttl, answer): + if not answer: + answer = host + self.logger.log ('%s:%s' % (answer, self.message)) + + def log (self, ip, message): + self.resolver.resolve_ptr ( + ip, + self.logger_thunk ( + message, + self.logger + ) + ) + +class unresolving_logger: + "Just in case you don't want to resolve" + def __init__ (self, logger): + self.logger = logger + + def log (self, ip, message): + self.logger.log ('%s:%s' % (ip, message)) + + +def strip_eol (line): + while line and line[-1] in '\r\n': + line = line[:-1] + return line + +class tail_logger: + "Keep track of the last <size> log messages" + def __init__ (self, logger, size=500): + self.size = size + self.logger = logger + self.messages = [] + + def log (self, message): + self.messages.append (strip_eol (message)) + if len (self.messages) > self.size: + del self.messages[0] + self.logger.log (message) diff --git a/demo/medusa054/m_syslog.py b/demo/medusa054/m_syslog.py new file mode 100644 index 0000000..e212766 --- /dev/null +++ b/demo/medusa054/m_syslog.py @@ -0,0 +1,182 @@ +# -*- Mode: Python -*- + +# ====================================================================== +# Copyright 1997 by Sam Rushing +# +# All Rights Reserved +# +# Permission to use, copy, modify, and distribute this software and +# its documentation for any purpose and without fee is hereby +# granted, provided that the above copyright notice appear in all +# copies and that both that copyright notice and this permission +# notice appear in supporting documentation, and that the name of Sam +# Rushing not be used in advertising or publicity pertaining to +# distribution of the software without specific, written prior +# permission. +# +# SAM RUSHING DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, +# INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN +# NO EVENT SHALL SAM RUSHING BE LIABLE FOR ANY SPECIAL, INDIRECT OR +# CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +# OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, +# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +# ====================================================================== + +"""socket interface to unix syslog. +On Unix, there are usually two ways of getting to syslog: via a +local unix-domain socket, or via the TCP service. + +Usually "/dev/log" is the unix domain socket. This may be different +for other systems. + +>>> my_client = syslog_client ('/dev/log') + +Otherwise, just use the UDP version, port 514. + +>>> my_client = syslog_client (('my_log_host', 514)) + +On win32, you will have to use the UDP version. Note that +you can use this to log to other hosts (and indeed, multiple +hosts). + +This module is not a drop-in replacement for the python +<syslog> extension module - the interface is different. + +Usage: + +>>> c = syslog_client() +>>> c = syslog_client ('/strange/non_standard_log_location') +>>> c = syslog_client (('other_host.com', 514)) +>>> c.log ('testing', facility='local0', priority='debug') + +""" + +# TODO: support named-pipe syslog. +# [see ftp://sunsite.unc.edu/pub/Linux/system/Daemons/syslog-fifo.tar.z] + +# from <linux/sys/syslog.h>: +# =========================================================================== +# priorities/facilities are encoded into a single 32-bit quantity, where the +# bottom 3 bits are the priority (0-7) and the top 28 bits are the facility +# (0-big number). Both the priorities and the facilities map roughly +# one-to-one to strings in the syslogd(8) source code. This mapping is +# included in this file. +# +# priorities (these are ordered) + +LOG_EMERG = 0 # system is unusable +LOG_ALERT = 1 # action must be taken immediately +LOG_CRIT = 2 # critical conditions +LOG_ERR = 3 # error conditions +LOG_WARNING = 4 # warning conditions +LOG_NOTICE = 5 # normal but significant condition +LOG_INFO = 6 # informational +LOG_DEBUG = 7 # debug-level messages + +# facility codes +LOG_KERN = 0 # kernel messages +LOG_USER = 1 # random user-level messages +LOG_MAIL = 2 # mail system +LOG_DAEMON = 3 # system daemons +LOG_AUTH = 4 # security/authorization messages +LOG_SYSLOG = 5 # messages generated internally by syslogd +LOG_LPR = 6 # line printer subsystem +LOG_NEWS = 7 # network news subsystem +LOG_UUCP = 8 # UUCP subsystem +LOG_CRON = 9 # clock daemon +LOG_AUTHPRIV = 10 # security/authorization messages (private) + +# other codes through 15 reserved for system use +LOG_LOCAL0 = 16 # reserved for local use +LOG_LOCAL1 = 17 # reserved for local use +LOG_LOCAL2 = 18 # reserved for local use +LOG_LOCAL3 = 19 # reserved for local use +LOG_LOCAL4 = 20 # reserved for local use +LOG_LOCAL5 = 21 # reserved for local use +LOG_LOCAL6 = 22 # reserved for local use +LOG_LOCAL7 = 23 # reserved for local use + +priority_names = { + "alert": LOG_ALERT, + "crit": LOG_CRIT, + "debug": LOG_DEBUG, + "emerg": LOG_EMERG, + "err": LOG_ERR, + "error": LOG_ERR, # DEPRECATED + "info": LOG_INFO, + "notice": LOG_NOTICE, + "panic": LOG_EMERG, # DEPRECATED + "warn": LOG_WARNING, # DEPRECATED + "warning": LOG_WARNING, + } + +facility_names = { + "auth": LOG_AUTH, + "authpriv": LOG_AUTHPRIV, + "cron": LOG_CRON, + "daemon": LOG_DAEMON, + "kern": LOG_KERN, + "lpr": LOG_LPR, + "mail": LOG_MAIL, + "news": LOG_NEWS, + "security": LOG_AUTH, # DEPRECATED + "syslog": LOG_SYSLOG, + "user": LOG_USER, + "uucp": LOG_UUCP, + "local0": LOG_LOCAL0, + "local1": LOG_LOCAL1, + "local2": LOG_LOCAL2, + "local3": LOG_LOCAL3, + "local4": LOG_LOCAL4, + "local5": LOG_LOCAL5, + "local6": LOG_LOCAL6, + "local7": LOG_LOCAL7, + } + +import socket + +class syslog_client: + def __init__ (self, address='/dev/log'): + self.address = address + self.stream = 0 + if isinstance(address, type('')): + try: + self.socket = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) + self.socket.connect(address) + except socket.error: + # Some Linux installations have /dev/log + # a stream socket instead of a datagram socket. + self.socket = socket.socket (socket.AF_UNIX, + socket.SOCK_STREAM) + self.stream = 1 + else: + self.socket = socket.socket (socket.AF_INET, socket.SOCK_DGRAM) + + # curious: when talking to the unix-domain '/dev/log' socket, a + # zero-terminator seems to be required. this string is placed + # into a class variable so that it can be overridden if + # necessary. + + log_format_string = '<%d>%s\000' + + def log (self, message, facility=LOG_USER, priority=LOG_INFO): + message = self.log_format_string % ( + self.encode_priority (facility, priority), + message + ) + if self.stream: + self.socket.send (message) + else: + self.socket.sendto (message, self.address) + + def encode_priority (self, facility, priority): + if type(facility) == type(''): + facility = facility_names[facility] + if type(priority) == type(''): + priority = priority_names[priority] + return (facility<<3) | priority + + def close (self): + if self.stream: + self.socket.close() diff --git a/demo/medusa054/medusa_gif.py b/demo/medusa054/medusa_gif.py new file mode 100644 index 0000000..005644a --- /dev/null +++ b/demo/medusa054/medusa_gif.py @@ -0,0 +1,8 @@ +# -*- Mode: Python -*- + +# the medusa icon as a python source file. + +width = 97 +height = 61 + +data = 'GIF89aa\000=\000\204\000\000\000\000\000\255\255\255\245\245\245ssskkkccc111)))\326\326\326!!!\316\316\316\300\300\300\204\204\000\224\224\224\214\214\214\200\200\200RRR\377\377\377JJJ\367\367\367BBB\347\347\347\000\204\000\020\020\020\265\265\265\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000!\371\004\001\000\000\021\000,\000\000\000\000a\000=\000\000\005\376`$\216di\236h\252\256l\353\276p,\317tm\337x\256\357|m\001@\240E\305\000\364\2164\206R)$\005\201\214\007r\012{X\255\312a\004\260\\>\026\3240\353)\224n\001W+X\334\373\231~\344.\303b\216\024\027x<\273\307\255G,rJiWN\014{S}k"?ti\013EdPQ\207G@_%\000\026yy\\\201\202\227\224<\221Fs$pOjWz\241<r@vO\236\231\233k\247M\2544\203F\177\235\236L#\247\256Z\270,\266BxJ[\276\256A]iE\304\305\262\273E\313\201\275i#\\\303\321\'h\203V\\\177\326\276\216\220P~\335\230_\264\013\342\275\344KF\233\360Q\212\352\246\000\367\274s\361\236\334\347T\341;\341\246\2202\177\3142\211`\242o\325@S\202\264\031\252\207\260\323\256\205\311\036\236\270\002\'\013\302\177\274H\010\324X\002\0176\212\037\376\321\360\032\226\207\244\2674(+^\202\346r\205J\0211\375\241Y#\256f\0127\315>\272\002\325\307g\012(\007\205\312#j\317(\012A\200\224.\241\003\346GS\247\033\245\344\264\366\015L\'PXQl]\266\263\243\232\260?\245\316\371\362\225\035\332\243J\273\332Q\263\357-D\241T\327\270\265\013W&\330\010u\371b\322IW0\214\261]\003\033Va\365Z#\207\213a\030k\2647\262\014p\354\024[n\321N\363\346\317\003\037P\000\235C\302\000\3228(\244\363YaA\005\022\255_\237@\260\000A\212\326\256qbp\321\332\266\011\334=T\023\010"!B\005\003A\010\224\020\220 H\002\337#\020 O\276E\357h\221\327\003\\\000b@v\004\351A.h\365\354\342B\002\011\257\025\\ \220\340\301\353\006\000\024\214\200pA\300\353\012\364\241k/\340\033C\202\003\000\310fZ\011\003V\240R\005\007\354\376\026A\000\000\360\'\202\177\024\004\210\003\000\305\215\360\000\000\015\220\240\332\203\027@\'\202\004\025VpA\000%\210x\321\206\032J\341\316\010\262\211H"l\333\341\200\200>"]P\002\212\011\010`\002\0066FP\200\001\'\024p]\004\027(8B\221\306]\000\201w>\002iB\001\007\340\260"v7J1\343(\257\020\251\243\011\242i\263\017\215\337\035\220\200\221\365m4d\015\016D\251\341iN\354\346Ng\253\200I\240\031\35609\245\2057\311I\302\2007t\231"&`\314\310\244\011e\226(\236\010w\212\300\234\011\012HX(\214\253\311@\001\233^\222pg{% \340\035\224&H\000\246\201\362\215`@\001"L\340\004\030\234\022\250\'\015(V:\302\235\030\240q\337\205\224\212h@\177\006\000\250\210\004\007\310\207\337\005\257-P\346\257\367]p\353\203\271\256:\203\236\211F\340\247\010\3329g\244\010\307*=A\000\203\260y\012\304s#\014\007D\207,N\007\304\265\027\021C\233\207%B\366[m\353\006\006\034j\360\306+\357\274a\204\000\000;' diff --git a/demo/medusa054/poison_handler.py b/demo/medusa054/poison_handler.py new file mode 100644 index 0000000..acc78ab --- /dev/null +++ b/demo/medusa054/poison_handler.py @@ -0,0 +1,69 @@ + +import string +import whrandom + +RESP_HEAD="""\ +<HTML><BODY BGCOLOR=\"#ffffff\"> +""" + +RESP_MIDDLE=""" +<h2>M2Crypto https server demonstration</h2> + +This web page is generated by the "poison" http request handler. +<br> +The links just go on and on and on... +<br><br> +""" + +RESP_TAIL=""" +</BODY></HTML> +""" + +charset='012345678/90ABCDEFGHIJKLM/NOPQRSTUVWXYZabcd/efghijklmnopqrs/tuvwxyz' +numchar=len(charset) + +def makepage(numlinks): + + title='<title>' + for u in range(whrandom.randint(3, 15)): + pick=whrandom.randint(0, numchar-1) + title=title+charset[pick] + title=title+'</title>' + + url='\r\n' + numlinks=whrandom.randint(2, numlinks) + for i in range(numlinks): + url=url+'<a href="/poison/' + for u in range(whrandom.randint(3, 15)): + pick=whrandom.randint(0, numchar-1) + ch=charset[pick] + if ch=='/' and url[-1]=='/': + ch=charset[pick+1] + url=url+ch + url=url+'/">' + for u in range(whrandom.randint(3, 15)): + pick=whrandom.randint(0, numchar-1) + url=url+charset[pick] + url=url+'</a><br>\r\n' + + url=RESP_HEAD+title+RESP_MIDDLE+url+RESP_TAIL + return url + + +class poison_handler: + """This is a clone of webpoison - every URL returns a page of URLs, each of which + returns a page of URLs, each of _which_ returns a page of URLs, ad infinitum. + The objective is to sucker address-harvesting bots run by spammers.""" + + def __init__(self, numlinks=10): + self.numlinks = numlinks + self.poison_level = 0 + + def match(self, request): + return (request.uri[:7] == '/poison') + + def handle_request(self, request): + if request.command == 'get': + request.push(makepage(self.numlinks)) + request.done() + diff --git a/demo/medusa054/producers.py b/demo/medusa054/producers.py new file mode 100644 index 0000000..a04906e --- /dev/null +++ b/demo/medusa054/producers.py @@ -0,0 +1,323 @@ +# -*- Mode: Python -*- + +""" +A collection of producers. +Each producer implements a particular feature: They can be combined +in various ways to get interesting and useful behaviors. + +For example, you can feed dynamically-produced output into the compressing +producer, then wrap this with the 'chunked' transfer-encoding producer. +""" + +import string +from asynchat import find_prefix_at_end + +class simple_producer: + "producer for a string" + def __init__ (self, data, buffer_size=1024): + self.data = data + self.buffer_size = buffer_size + + def more (self): + if len (self.data) > self.buffer_size: + result = self.data[:self.buffer_size] + self.data = self.data[self.buffer_size:] + return result + else: + result = self.data + self.data = '' + return result + +class scanning_producer: + "like simple_producer, but more efficient for large strings" + def __init__ (self, data, buffer_size=1024): + self.data = data + self.buffer_size = buffer_size + self.pos = 0 + + def more (self): + if self.pos < len(self.data): + lp = self.pos + rp = min ( + len(self.data), + self.pos + self.buffer_size + ) + result = self.data[lp:rp] + self.pos = self.pos + len(result) + return result + else: + return '' + +class lines_producer: + "producer for a list of lines" + + def __init__ (self, lines): + self.lines = lines + + def more (self): + if self.lines: + chunk = self.lines[:50] + self.lines = self.lines[50:] + return string.join (chunk, '\r\n') + '\r\n' + else: + return '' + +class buffer_list_producer: + "producer for a list of strings" + + # i.e., data == string.join (buffers, '') + + def __init__ (self, buffers): + + self.index = 0 + self.buffers = buffers + + def more (self): + if self.index >= len(self.buffers): + return '' + else: + data = self.buffers[self.index] + self.index = self.index + 1 + return data + +class file_producer: + "producer wrapper for file[-like] objects" + + # match http_channel's outgoing buffer size + out_buffer_size = 1<<16 + + def __init__ (self, file): + self.done = 0 + self.file = file + + def more (self): + if self.done: + return '' + else: + data = self.file.read (self.out_buffer_size) + if not data: + self.file.close() + del self.file + self.done = 1 + return '' + else: + return data + +# A simple output producer. This one does not [yet] have +# the safety feature builtin to the monitor channel: runaway +# output will not be caught. + +# don't try to print from within any of the methods +# of this object. + +class output_producer: + "Acts like an output file; suitable for capturing sys.stdout" + def __init__ (self): + self.data = '' + + def write (self, data): + lines = string.splitfields (data, '\n') + data = string.join (lines, '\r\n') + self.data = self.data + data + + def writeline (self, line): + self.data = self.data + line + '\r\n' + + def writelines (self, lines): + self.data = self.data + string.joinfields ( + lines, + '\r\n' + ) + '\r\n' + + def flush (self): + pass + + def softspace (self, *args): + pass + + def more (self): + if self.data: + result = self.data[:512] + self.data = self.data[512:] + return result + else: + return '' + +class composite_producer: + "combine a fifo of producers into one" + def __init__ (self, producers): + self.producers = producers + + def more (self): + while len(self.producers): + p = self.producers[0] + d = p.more() + if d: + return d + else: + self.producers.pop(0) + else: + return '' + + +class globbing_producer: + """ + 'glob' the output from a producer into a particular buffer size. + helps reduce the number of calls to send(). [this appears to + gain about 30% performance on requests to a single channel] + """ + + def __init__ (self, producer, buffer_size=1<<16): + self.producer = producer + self.buffer = '' + self.buffer_size = buffer_size + + def more (self): + while len(self.buffer) < self.buffer_size: + data = self.producer.more() + if data: + self.buffer = self.buffer + data + else: + break + r = self.buffer + self.buffer = '' + return r + + +class hooked_producer: + """ + A producer that will call <function> when it empties,. + with an argument of the number of bytes produced. Useful + for logging/instrumentation purposes. + """ + + def __init__ (self, producer, function): + self.producer = producer + self.function = function + self.bytes = 0 + + def more (self): + if self.producer: + result = self.producer.more() + if not result: + self.producer = None + self.function (self.bytes) + else: + self.bytes = self.bytes + len(result) + return result + else: + return '' + +# HTTP 1.1 emphasizes that an advertised Content-Length header MUST be +# correct. In the face of Strange Files, it is conceivable that +# reading a 'file' may produce an amount of data not matching that +# reported by os.stat() [text/binary mode issues, perhaps the file is +# being appended to, etc..] This makes the chunked encoding a True +# Blessing, and it really ought to be used even with normal files. +# How beautifully it blends with the concept of the producer. + +class chunked_producer: + """A producer that implements the 'chunked' transfer coding for HTTP/1.1. + Here is a sample usage: + request['Transfer-Encoding'] = 'chunked' + request.push ( + producers.chunked_producer (your_producer) + ) + request.done() + """ + + def __init__ (self, producer, footers=None): + self.producer = producer + self.footers = footers + + def more (self): + if self.producer: + data = self.producer.more() + if data: + return '%x\r\n%s\r\n' % (len(data), data) + else: + self.producer = None + if self.footers: + return string.join ( + ['0'] + self.footers, + '\r\n' + ) + '\r\n\r\n' + else: + return '0\r\n\r\n' + else: + return '' + +# Unfortunately this isn't very useful right now (Aug 97), because +# apparently the browsers don't do on-the-fly decompression. Which +# is sad, because this could _really_ speed things up, especially for +# low-bandwidth clients (i.e., most everyone). + +try: + import zlib +except ImportError: + zlib = None + +class compressed_producer: + """ + Compress another producer on-the-fly, using ZLIB + [Unfortunately, none of the current browsers seem to support this] + """ + + # Note: It's not very efficient to have the server repeatedly + # compressing your outgoing files: compress them ahead of time, or + # use a compress-once-and-store scheme. However, if you have low + # bandwidth and low traffic, this may make more sense than + # maintaining your source files compressed. + # + # Can also be used for compressing dynamically-produced output. + + def __init__ (self, producer, level=5): + self.producer = producer + self.compressor = zlib.compressobj (level) + + def more (self): + if self.producer: + cdata = '' + # feed until we get some output + while not cdata: + data = self.producer.more() + if not data: + self.producer = None + return self.compressor.flush() + else: + cdata = self.compressor.compress (data) + return cdata + else: + return '' + +class escaping_producer: + + "A producer that escapes a sequence of characters" + " Common usage: escaping the CRLF.CRLF sequence in SMTP, NNTP, etc..." + + def __init__ (self, producer, esc_from='\r\n.', esc_to='\r\n..'): + self.producer = producer + self.esc_from = esc_from + self.esc_to = esc_to + self.buffer = '' + self.find_prefix_at_end = find_prefix_at_end + + def more (self): + esc_from = self.esc_from + esc_to = self.esc_to + + buffer = self.buffer + self.producer.more() + + if buffer: + buffer = string.replace (buffer, esc_from, esc_to) + i = self.find_prefix_at_end (buffer, esc_from) + if i: + # we found a prefix + self.buffer = buffer[-i:] + return buffer[:-i] + else: + # no prefix, return it all + self.buffer = '' + return buffer + else: + return buffer diff --git a/demo/medusa054/server.pem b/demo/medusa054/server.pem new file mode 100644 index 0000000..1ee9282 --- /dev/null +++ b/demo/medusa054/server.pem @@ -0,0 +1,36 @@ +-----BEGIN CERTIFICATE----- +MIIDYjCCAsugAwIBAgIBBDANBgkqhkiG9w0BAQQFADCBgDELMAkGA1UEBhMCU0cx +ETAPBgNVBAoTCE0yQ3J5cHRvMRQwEgYDVQQLEwtNMkNyeXB0byBDQTEkMCIGA1UE +AxMbTTJDcnlwdG8gQ2VydGlmaWNhdGUgTWFzdGVyMSIwIAYJKoZIhvcNAQkBFhNu +Z3BzQG5ldG1lbWV0aWMuY29tMB4XDTAzMDYyMjEzMzAxNFoXDTA0MDYyMTEzMzAx +NFowXTELMAkGA1UEBhMCU0cxETAPBgNVBAoTCE0yQ3J5cHRvMRIwEAYDVQQDEwls +b2NhbGhvc3QxJzAlBgkqhkiG9w0BCQEWGGFkbWluQHNlcnZlci5leGFtcGxlLmRv +bTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA37aKGZtFicl8xXXTLJ8/JD7c +kd3t/teCX9i61lpaDQCKBoVrrursIvZihemMKI9g/u/+BLqt5g8mBdgUdYz0txc8 +KEbV2hj+wwOX4H3XwD0Y+DysXiNHq7/tFdmzSVHoLxpY4zYzXbxQ/p049wvIyPRp +/y3omcnx/TEUhkn+JmkCAwEAAaOCAQwwggEIMAkGA1UdEwQCMAAwLAYJYIZIAYb4 +QgENBB8WHU9wZW5TU0wgR2VuZXJhdGVkIENlcnRpZmljYXRlMB0GA1UdDgQWBBTB +H/mUYlww24mJlSxEtGdlwojO9zCBrQYDVR0jBIGlMIGigBTr+pwHMS1CmF9cuGI4 +du3YqoHAwqGBhqSBgzCBgDELMAkGA1UEBhMCU0cxETAPBgNVBAoTCE0yQ3J5cHRv +MRQwEgYDVQQLEwtNMkNyeXB0byBDQTEkMCIGA1UEAxMbTTJDcnlwdG8gQ2VydGlm +aWNhdGUgTWFzdGVyMSIwIAYJKoZIhvcNAQkBFhNuZ3BzQG5ldG1lbWV0aWMuY29t +ggEAMA0GCSqGSIb3DQEBBAUAA4GBAAvl6v0s3eFeGP4iAcrfysuK7jzFKhjDYuOy +lVS3u33bZNLnMpM6OSEM9yPh4WpFCVHf+nYwC71pk4ilsLVXjKxymm2lNGcxLVuW +iydFz4Ly9nmN7Ja9ygYT39dGAFP/wN7ELTpsbul8VfmqhNg9y81d8i/A1tK3AGA8 +0QkPQNdP +-----END CERTIFICATE----- +-----BEGIN RSA PRIVATE KEY----- +MIICXAIBAAKBgQDftooZm0WJyXzFddMsnz8kPtyR3e3+14Jf2LrWWloNAIoGhWuu +6uwi9mKF6Ywoj2D+7/4Euq3mDyYF2BR1jPS3FzwoRtXaGP7DA5fgfdfAPRj4PKxe +I0erv+0V2bNJUegvGljjNjNdvFD+nTj3C8jI9Gn/LeiZyfH9MRSGSf4maQIDAQAB +AoGAHpeVtv62uarl9xKvuBBm0AwQmZnhq9HIsFaw5hMg8Vo7hbzFBvx1IirTOkC/ +u+QvfW1QLVFh6m3z4ySzV4fZBtcd6F9SbSrZ0xsxIUB2NOSa1RGgiaP61bJnMMM1 +xM3O9iwM5GZc3Gqy08QOCpDl0772VJ+9Gz3FA88mrc6rHQkCQQDz6RIatFjT28n8 +1vy0nHwwZz2oXTpe/pyZPwoKj8zVsmrKhKwOw7l8ArxjP8zoHOE7AQBCXYDMNoFp +IAF0yuqrAkEA6s0wMEdPpQeb0XHAfccyJQoeULxHdVFoz1wWmGSOm4YmQtR8/QJx +luEgfpeRkzxBKt5Ls3MEkheIOw7xV24zOwJAMz+DaE0AZPNHS3K4ghJnHZxzng6I +lzEUIjbWm0V/ml70hTy/EhMZw+6nOotLOHHo+QbK0SboSwAgzL/Gzo1cJQJANqpS +38qadleRJXAQWrg3qnvyluVe1aeAeVZ9RDmVIgxXeBO0jcs12uTLBe4PzHGo0mwy +v7K1i7XC180gzzQu5QJBAOxITT9RoWSSozPvnirHd37sn+RsrNYkV07NAa80M20Z +DkBPHeMVkNgigrQ6L6vWmbRDGQbGcMplAxnI5ppKCoU= +-----END RSA PRIVATE KEY----- diff --git a/demo/medusa054/status_handler.py b/demo/medusa054/status_handler.py new file mode 100644 index 0000000..8d2583e --- /dev/null +++ b/demo/medusa054/status_handler.py @@ -0,0 +1,272 @@ +# -*- Mode: Python -*- + +VERSION_STRING = "$Id: status_handler.py 299 2005-06-09 17:32:28Z heikki $" + +# +# medusa status extension +# + +import string +import time +import re +from cgi import escape + +import asyncore +import http_server +import medusa_gif +import producers +from counter import counter + +START_TIME = long(time.time()) + +class status_extension: + hit_counter = counter() + + def __init__ (self, objects, statusdir='/status', allow_emergency_debug=0): + self.objects = objects + self.statusdir = statusdir + self.allow_emergency_debug = allow_emergency_debug + # We use /status instead of statusdir here because it's too + # hard to pass statusdir to the logger, who makes the HREF + # to the object dir. We don't need the security-through- + # obscurity here in any case, because the id is obscurity enough + self.hyper_regex = re.compile('/status/object/([0-9]+)/.*') + self.hyper_objects = [] + for object in objects: + self.register_hyper_object (object) + + def __repr__ (self): + return '<Status Extension (%s hits) at %x>' % ( + self.hit_counter, + id(self) + ) + + def match (self, request): + path, params, query, fragment = request.split_uri() + # For reasons explained above, we don't use statusdir for /object + return (path[:len(self.statusdir)] == self.statusdir or + path[:len("/status/object/")] == '/status/object/') + + # Possible Targets: + # /status + # /status/channel_list + # /status/medusa.gif + + # can we have 'clickable' objects? + # [yes, we can use id(x) and do a linear search] + + # Dynamic producers: + # HTTP/1.0: we must close the channel, because it's dynamic output + # HTTP/1.1: we can use the chunked transfer-encoding, and leave + # it open. + + def handle_request (self, request): + [path, params, query, fragment] = request.split_uri() + self.hit_counter.increment() + if path == self.statusdir: # and not a subdirectory + up_time = string.join (english_time (long(time.time()) - START_TIME)) + request['Content-Type'] = 'text/html' + request.push ( + '<html>' + '<title>Medusa Status Reports</title>' + '<body bgcolor="#ffffff">' + '<h1>Medusa Status Reports</h1>' + '<b>Up:</b> %s' % up_time + ) + for i in range(len(self.objects)): + request.push (self.objects[i].status()) + request.push ('<hr>\r\n') + request.push ( + '<p><a href="%s/channel_list">Channel List</a>' + '<hr>' + '<img src="%s/medusa.gif" align=right width=%d height=%d>' + '</body></html>' % ( + self.statusdir, + self.statusdir, + medusa_gif.width, + medusa_gif.height + ) + ) + request.done() + elif path == self.statusdir + '/channel_list': + request['Content-Type'] = 'text/html' + request.push ('<html><body>') + request.push(channel_list_producer(self.statusdir)) + request.push ( + '<hr>' + '<img src="%s/medusa.gif" align=right width=%d height=%d>' % ( + self.statusdir, + medusa_gif.width, + medusa_gif.height + ) + + '</body></html>' + ) + request.done() + + elif path == self.statusdir + '/medusa.gif': + request['Content-Type'] = 'image/gif' + request['Content-Length'] = len(medusa_gif.data) + request.push (medusa_gif.data) + request.done() + + elif path == self.statusdir + '/close_zombies': + message = ( + '<h2>Closing all zombie http client connections...</h2>' + '<p><a href="%s">Back to the status page</a>' % self.statusdir + ) + request['Content-Type'] = 'text/html' + request['Content-Length'] = len (message) + request.push (message) + now = int (time.time()) + for channel in asyncore.socket_map.keys(): + if channel.__class__ == http_server.http_channel: + if channel != request.channel: + if (now - channel.creation_time) > channel.zombie_timeout: + channel.close() + request.done() + + # Emergency Debug Mode + # If a server is running away from you, don't KILL it! + # Move all the AF_INET server ports and perform an autopsy... + # [disabled by default to protect the innocent] + elif self.allow_emergency_debug and path == self.statusdir + '/emergency_debug': + request.push ('<html>Moving All Servers...</html>') + request.done() + for channel in asyncore.socket_map.keys(): + if channel.accepting: + if type(channel.addr) is type(()): + ip, port = channel.addr + channel.socket.close() + channel.del_channel() + channel.addr = (ip, port+10000) + fam, typ = channel.family_and_type + channel.create_socket (fam, typ) + channel.set_reuse_addr() + channel.bind (channel.addr) + channel.listen(5) + + else: + m = self.hyper_regex.match (path) + if m: + oid = string.atoi (m.group (1)) + for object in self.hyper_objects: + if id (object) == oid: + if hasattr (object, 'hyper_respond'): + object.hyper_respond (self, path, request) + else: + request.error (404) + return + + def status (self): + return producers.simple_producer ( + '<li>Status Extension <b>Hits</b> : %s' % self.hit_counter + ) + + def register_hyper_object (self, object): + if not object in self.hyper_objects: + self.hyper_objects.append (object) + +import logger + +class logger_for_status (logger.tail_logger): + + def status (self): + return 'Last %d log entries for: %s' % ( + len (self.messages), + html_repr (self) + ) + + def hyper_respond (self, sh, path, request): + request['Content-Type'] = 'text/plain' + messages = self.messages[:] + messages.reverse() + request.push (lines_producer (messages)) + request.done() + +class lines_producer: + def __init__ (self, lines): + self.lines = lines + + def more (self): + if self.lines: + chunk = self.lines[:50] + self.lines = self.lines[50:] + return string.join (chunk, '\r\n') + '\r\n' + else: + return '' + +class channel_list_producer (lines_producer): + def __init__ (self, statusdir): + channel_reprs = map ( + lambda x: '<' + repr(x)[1:-1] + '>', + asyncore.socket_map.values() + ) + channel_reprs.sort() + lines_producer.__init__ ( + self, + ['<h1>Active Channel List</h1>', + '<pre>' + ] + channel_reprs + [ + '</pre>', + '<p><a href="%s">Status Report</a>' % statusdir + ] + ) + + +def html_repr (object): + so = escape (repr (object)) + if hasattr (object, 'hyper_respond'): + return '<a href="/status/object/%d/">%s</a>' % (id (object), so) + else: + return so + +def html_reprs (list, front='', back=''): + reprs = map ( + lambda x,f=front,b=back: '%s%s%s' % (f,x,b), + map (lambda x: escape (html_repr(x)), list) + ) + reprs.sort() + return reprs + +# for example, tera, giga, mega, kilo +# p_d (n, (1024, 1024, 1024, 1024)) +# smallest divider goes first - for example +# minutes, hours, days +# p_d (n, (60, 60, 24)) + +def progressive_divide (n, parts): + result = [] + for part in parts: + n, rem = divmod (n, part) + result.append (rem) + result.append (n) + return result + +# b,k,m,g,t +def split_by_units (n, units, dividers, format_string): + divs = progressive_divide (n, dividers) + result = [] + for i in range(len(units)): + if divs[i]: + result.append (format_string % (divs[i], units[i])) + result.reverse() + if not result: + return [format_string % (0, units[0])] + else: + return result + +def english_bytes (n): + return split_by_units ( + n, + ('','K','M','G','T'), + (1024, 1024, 1024, 1024, 1024), + '%d %sB' + ) + +def english_time (n): + return split_by_units ( + n, + ('secs', 'mins', 'hours', 'days', 'weeks', 'years'), + ( 60, 60, 24, 7, 52), + '%d %s' + ) diff --git a/demo/medusa054/xmlrpc_handler.py b/demo/medusa054/xmlrpc_handler.py new file mode 100644 index 0000000..15057cf --- /dev/null +++ b/demo/medusa054/xmlrpc_handler.py @@ -0,0 +1,103 @@ +# -*- Mode: Python -*- + +# See http://www.xml-rpc.com/ +# http://www.pythonware.com/products/xmlrpc/ + +# Based on "xmlrpcserver.py" by Fredrik Lundh (fredrik@pythonware.com) + +VERSION = "$Id: xmlrpc_handler.py 299 2005-06-09 17:32:28Z heikki $" + +import http_server +import xmlrpclib + +import string +import sys + +class xmlrpc_handler: + + def match (self, request): + # Note: /RPC2 is not required by the spec, so you may override this method. + if request.uri[:5] == '/RPC2': + return 1 + else: + return 0 + + def handle_request (self, request): + [path, params, query, fragment] = request.split_uri() + + if request.command == 'POST': + request.collector = collector (self, request) + else: + request.error (400) + + def continue_request (self, data, request): + params, method = xmlrpclib.loads (data) + try: + # generate response + try: + response = self.call (method, params) + if type(response) != type(()): + response = (response,) + except: + # report exception back to server + response = xmlrpclib.dumps ( + xmlrpclib.Fault (1, "%s:%s" % (sys.exc_type, sys.exc_value)) + ) + else: + response = xmlrpclib.dumps (response, methodresponse=1) + except: + # internal error, report as HTTP server error + request.error (500) + else: + # got a valid XML RPC response + request['Content-Type'] = 'text/xml' + request.push (response) + request.done() + + def call (self, method, params): + # override this method to implement RPC methods + raise "NotYetImplemented" + +class collector: + + "gathers input for POST and PUT requests" + + def __init__ (self, handler, request): + + self.handler = handler + self.request = request + self.data = '' + + # make sure there's a content-length header + cl = request.get_header ('content-length') + + if not cl: + request.error (411) + else: + cl = string.atoi (cl) + # using a 'numeric' terminator + self.request.channel.set_terminator (cl) + + def collect_incoming_data (self, data): + self.data = self.data + data + + def found_terminator (self): + # set the terminator back to the default + self.request.channel.set_terminator ('\r\n\r\n') + self.handler.continue_request (self.data, self.request) + +if __name__ == '__main__': + + class rpc_demo (xmlrpc_handler): + + def call (self, method, params): + print 'method="%s" params=%s' % (method, params) + return "Sure, that works" + + import asyncore + + hs = http_server.http_server ('', 8000) + rpc = rpc_demo() + hs.install_handler (rpc) + + asyncore.loop() diff --git a/demo/perf/memio.py b/demo/perf/memio.py new file mode 100644 index 0000000..238d85c --- /dev/null +++ b/demo/perf/memio.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python2.0 + +"""A comparison of Python's cStringIO and M2Crypto's MemoryBuffer, +the outcome of which is that MemoryBuffer suffers from doing too much +in Python. + +Two way to optimise MemoryBuffer: +1. Create MemoryBufferIn and MemoryBufferOut a la StringI and StringO. +2. Have MemoryBuffer do all internal work with cStringIO. ;-) +""" + +from cStringIO import StringIO +from M2Crypto.BIO import MemoryBuffer +from M2Crypto import m2 +import profile + +txt = 'Python, Smalltalk, Haskell, Scheme, Lisp, Self, Erlang, ML, ...' + +def stringi(iter, txt=txt): + buf = StringIO() + for i in range(iter): + buf.write(txt) + out = buf.getvalue() + +def membufi(iter, txt=txt): + buf = MemoryBuffer() + for i in range(iter): + buf.write(txt) + out = buf.getvalue() + +def membuf2i(iter, txt=txt): + buf = MemoryBuffer() + buf.write(txt * iter) + out = buf.getvalue() + +def cmembufi(iter, txt=txt): + buf = m2.bio_new(m2.bio_s_mem()) + for i in range(iter): + m2.bio_write(buf, txt) + m2.bio_set_mem_eof_return(buf, 0) + out = m2.bio_read(buf, m2.bio_ctrl_pending(buf)) + +if __name__ == '__main__': + profile.run('stringi(10000)') + profile.run('cmembufi(10000)') + profile.run('membufi(10000)') + profile.run('membuf2i(10000)') + + diff --git a/demo/perf/sha1.py b/demo/perf/sha1.py new file mode 100644 index 0000000..6d85289 --- /dev/null +++ b/demo/perf/sha1.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python2.0 + +"""A comparison of Python's sha and M2Crypto.EVP.MessageDigest, +the outcome of which is that EVP.MessageDigest suffers from doing +too much in Python.""" + +import profile + +from sha import sha +import M2Crypto +from M2Crypto import m2 +from M2Crypto.EVP import MessageDigest + +txt = 'Python, Smalltalk, Haskell, Scheme, Lisp, Self, Erlang, ML, ...' + +def py_sha(iter, txt=txt): + s = sha() + for i in range(iter): + s.update(txt) + out = s.digest() + +def m2_sha(iter, txt=txt): + s = MessageDigest('sha1') + for i in range(iter): + s.update(txt) + out = s.digest() + +def m2_sha_2(iter, txt=txt): + s = MessageDigest('sha1') + s.update(txt * iter) + out = s.digest() + +def m2c_sha(iter, txt=txt): + ctx = m2.md_ctx_new() + m2.digest_init(ctx, m2.sha1()) + for i in range(iter): + m2.digest_update(ctx, txt) + out = m2.digest_final(ctx) + +if __name__ == '__main__': + profile.run('py_sha(10000)') + profile.run('m2_sha(10000)') + profile.run('m2_sha_2(10000)') + profile.run('m2c_sha(10000)') + + diff --git a/demo/pgp/pgpstep.py b/demo/pgp/pgpstep.py new file mode 100644 index 0000000..ff5b135 --- /dev/null +++ b/demo/pgp/pgpstep.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python + +"""pgpstep - steps through a pgp2 packet stream. + +Copyright (c) 1999 Ng Pheng Siong. All rights reserved.""" + +from M2Crypto import PGP, util +import time + +def desc_public_key(pkt): + print 'packet = public_key' + print 'version =', pkt.version() + print 'created = ', time.asctime(time.gmtime(pkt.timestamp())) + print 'validity code =', pkt.validity() + print 'pkc type =', `pkt.pkc()` + #e, n = pkt.pubkey() + print 'e =', `pkt._e` + print 'n =', `pkt._n` + print + +def desc_trust(pkt): + print 'packet = trust' + print 'trustworthiness = <ignored>' + print + +def desc_userid(pkt): + print 'packet = user_id' + print 'user_id =', pkt.userid() + print + +def desc_signature(pkt): + print 'packet = signature' + print 'version =', pkt.version() + print 'classification =', `pkt._classification` + print 'created = ', time.asctime(time.gmtime(pkt.timestamp())) + print 'keyid =', `pkt._keyid` + print 'pkc type =', `pkt.pkc()` + print 'md_algo =', `pkt._md_algo` + print 'md_chksum =', `pkt._md_chksum` + print 'sig =', `pkt._sig` + print + +def desc_private_key(pkt): + print 'packet = private key' + print 'version =', pkt.version() + print 'created = ', time.asctime(time.gmtime(pkt.timestamp())) + print 'validity code =', pkt.validity() + print 'pkc type =', `pkt.pkc()` + print 'e =', `pkt._e` + print 'n =', `pkt._n` + print 'cipher =', `pkt._cipher` + if pkt._cipher == '\001': + print 'following attributes are encrypted' + print 'iv =', `pkt._iv` + print 'd =', `pkt._d` + print 'p =', `pkt._p` + print 'q =', `pkt._q` + print 'u =', `pkt._u` + print 'checksum =', `pkt._cksum` + print + +def desc_cke(pkt): + print 'packet = cke' + print 'iv =', `pkt.iv` + print 'checksum =', `pkt.cksum` + print 'ciphertext =', `pkt.ctxt` + print + +def desc_pke(pkt): + print 'packet = pke' + print 'version =', pkt.version + print 'keyid =', `pkt.keyid` + print 'pkc type =', pkt.pkc_type + print 'dek =', hex(pkt.dek)[:-1] + print + +def desc_literal(pkt): + print 'packet = literal data' + print 'mode =', `pkt.fmode` + print 'filename =', pkt.fname + print 'time = ', time.asctime(time.gmtime(pkt.ftime)) + print 'data = <%d octets of literal data>' % (len(pkt.data),) + print + +DESC = { + PGP.public_key_packet: desc_public_key, + PGP.trust_packet: desc_trust, + PGP.userid_packet: desc_userid, + PGP.signature_packet: desc_signature, + PGP.private_key_packet: desc_private_key, + PGP.cke_packet: desc_cke, + PGP.pke_packet: desc_pke, + PGP.literal_packet: desc_literal, +} + +if __name__ == '__main__': + import sys + count = 0 + for arg in sys.argv[1:]: + f = open(arg, 'rb') + ps = PGP.packet_stream(f) + while 1: + pkt = ps.read() + if pkt is None: + break + elif pkt: + print '-'*70 + DESC[pkt.__class__](pkt) + count = count + ps.count() + ps.close() + print '-'*70 + print 'Total octets processed =', count + diff --git a/demo/pgp/pubring.pgp b/demo/pgp/pubring.pgp Binary files differnew file mode 100644 index 0000000..fbe4863 --- /dev/null +++ b/demo/pgp/pubring.pgp diff --git a/demo/pgp/secring.pgp b/demo/pgp/secring.pgp Binary files differnew file mode 100644 index 0000000..7eca123 --- /dev/null +++ b/demo/pgp/secring.pgp diff --git a/demo/pkcs7/pkcs7-thawte.pem b/demo/pkcs7/pkcs7-thawte.pem new file mode 100644 index 0000000..a444627 --- /dev/null +++ b/demo/pkcs7/pkcs7-thawte.pem @@ -0,0 +1,45 @@ +-----BEGIN PKCS7----- +MIAGCSqGSIb3DQEHAqCAMIIH1wIBATELMAkGBSsOAwIaBQAwgAYJKoZIhvcNAQcB +AACgggXgMIICxDCCAi2gAwIBAgIDAphPMA0GCSqGSIb3DQEBBAUAMIGUMQswCQYD +VQQGEwJaQTEVMBMGA1UECBMMV2VzdGVybiBDYXBlMRQwEgYDVQQHEwtEdXJiYW52 +aWxsZTEPMA0GA1UEChMGVGhhd3RlMR0wGwYDVQQLExRDZXJ0aWZpY2F0ZSBTZXJ2 +aWNlczEoMCYGA1UEAxMfUGVyc29uYWwgRnJlZW1haWwgUlNBIDE5OTkuOS4xNjAe +Fw0wMDA1MTgxMTA4MTJaFw0wMTA1MTgxMTA4MTJaMGUxETAPBgNVBAQTCFN0cm9l +ZGVyMRAwDgYDVQQqEwdNaWNoYWVsMRkwFwYDVQQDExBNaWNoYWVsIFN0cm9lZGVy +MSMwIQYJKoZIhvcNAQkBFhRtaWNoYWVsQHN0cm9lZGVyLmNvbTCBnzANBgkqhkiG +9w0BAQEFAAOBjQAwgYkCgYEAqzxf+ZpWoELOD7sXes/G9O6dSCkGeNL/G5l0k/b4 +C9QnZyEh2S0vWI+g43G1uGhu6sgI5Y1SfrG08xGIUEcmexxnl6krTR/QlHr+t9If +UXFTmEPAnuIpfPRUqhOQlNFB1DfwKVLb4J3b2947ulv4vpb4owVMC+WjDM5fo765 +RXMCAwEAAaNSMFAwHwYDVR0RBBgwFoEUbWljaGFlbEBzdHJvZWRlci5jb20wDAYD +VR0TAQH/BAIwADAfBgNVHSMEGDAWgBSIq/Fgg2ZV9ORYx0YdwGG9I9fDjDANBgkq +hkiG9w0BAQQFAAOBgQCxx7Uvsnq1F/yOIPiUmlaTXLFAvvTj2trHAF4UFhkQGe6V +IBHmDk62H1xJhA4AHNVz3Pe250S8Fu75OgOh2pRJ8KfB6cJU0gxz9ahRt48Qy6+m +XUHk9135ru2u8I505nj4i37T6zK3+O/lZU+P79qdPHYwO6dNsYyaRFYLxGyXTjCC +AxQwggJ9oAMCAQICAQswDQYJKoZIhvcNAQEEBQAwgdExCzAJBgNVBAYTAlpBMRUw +EwYDVQQIEwxXZXN0ZXJuIENhcGUxEjAQBgNVBAcTCUNhcGUgVG93bjEaMBgGA1UE +ChMRVGhhd3RlIENvbnN1bHRpbmcxKDAmBgNVBAsTH0NlcnRpZmljYXRpb24gU2Vy +dmljZXMgRGl2aXNpb24xJDAiBgNVBAMTG1RoYXd0ZSBQZXJzb25hbCBGcmVlbWFp +bCBDQTErMCkGCSqGSIb3DQEJARYccGVyc29uYWwtZnJlZW1haWxAdGhhd3RlLmNv +bTAeFw05OTA5MTYxNDAxNDBaFw0wMTA5MTUxNDAxNDBaMIGUMQswCQYDVQQGEwJa +QTEVMBMGA1UECBMMV2VzdGVybiBDYXBlMRQwEgYDVQQHEwtEdXJiYW52aWxsZTEP +MA0GA1UEChMGVGhhd3RlMR0wGwYDVQQLExRDZXJ0aWZpY2F0ZSBTZXJ2aWNlczEo +MCYGA1UEAxMfUGVyc29uYWwgRnJlZW1haWwgUlNBIDE5OTkuOS4xNjCBnzANBgkq +hkiG9w0BAQEFAAOBjQAwgYkCgYEAs2lal9TQFgt6tcVd6SGcI3LNEkxL937Px/vK +ciT0QlKsV5Xje2F6F4Tn/XI5OJS06u1lp5IGXr3gZfYZu5R5dkw+uWhwdYQc9BF0 +ALwFLE8JAxcxzPRB1HLGpl3iiESwiy7ETfHw1oU+bPOVlHiRfkDpnNGNFVeOwnPl +MN5G9U8CAwEAAaM3MDUwEgYDVR0TAQH/BAgwBgEB/wIBADAfBgNVHSMEGDAWgBRy +ScJzNMZV9At2coF+d/SH58ayDjANBgkqhkiG9w0BAQQFAAOBgQBrxlnpMfrptuyx +A9jfcnL+kWBI6sZV3XvwZ47GYXDnbcKlN9idtxcoVgWL3Vx1b8aRkMZsZnET0BB8 +a5FvhuAhNi3B1+qyCa3PLW3Gg1Kb+7v+nIed/LfpdJLkXJeu/H6syg1vcnpnLGtz +9Yb5nfUAbvQdB86dnoJjKe+TCX5V3jGCAdAwggHMAgEBMIGcMIGUMQswCQYDVQQG +EwJaQTEVMBMGA1UECBMMV2VzdGVybiBDYXBlMRQwEgYDVQQHEwtEdXJiYW52aWxs +ZTEPMA0GA1UEChMGVGhhd3RlMR0wGwYDVQQLExRDZXJ0aWZpY2F0ZSBTZXJ2aWNl +czEoMCYGA1UEAxMfUGVyc29uYWwgRnJlZW1haWwgUlNBIDE5OTkuOS4xNgIDAphP +MAkGBSsOAwIaBQCggYowGAYJKoZIhvcNAQkDMQsGCSqGSIb3DQEHATAcBgkqhkiG +9w0BCQUxDxcNMDAwNzE2MTQ0MzQ0WjAjBgkqhkiG9w0BCQQxFgQU2jmj7l5rSw0y +Vb/vlWAYkK/YBwkwKwYJKoZIhvcNAQkPMR4wHDAKBggqhkiG9w0DBzAOBggqhkiG +9w0DAgICAIAwDQYJKoZIhvcNAQEBBQAEgYBagl1y4nRwW+GLhZ5tElkh9Z/yFirz +ZXI0jMywML/ADxsAf1yV2JIYCCk0V33QdrYy8MVqgErGXxGdA5aKnm5545Wf4Noz +CLWF4FJc6RDSFp7dv2631TrswdFRRCHNds+dvP5v5EAiADvmEOYaDiWM07jp73jx +TkkzxW9cevZDaQAAAAA= +-----END PKCS7----- diff --git a/demo/pkcs7/test.py b/demo/pkcs7/test.py new file mode 100644 index 0000000..afa7477 --- /dev/null +++ b/demo/pkcs7/test.py @@ -0,0 +1,6 @@ +from M2Crypto import BIO, SMIME + +pf = BIO.openfile('pkcs7-thawte.pem') +p7 = SMIME.load_pkcs7_bio(pf) +print p7.type(1) + diff --git a/demo/rsa.priv.pem b/demo/rsa.priv.pem new file mode 100644 index 0000000..ed34db6 --- /dev/null +++ b/demo/rsa.priv.pem @@ -0,0 +1,9 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIBOwIBAAJBANQNY7RD9BarYRsmMazM1hd7a+u3QeMPFZQ7Ic+BmmeWHvvVP4Yj +yu1t6vAut7mKkaDeKbT3yiGVUgAEUaWMXqECAwEAAQJAIHCz8h37N4ScZHThYJgt +oIYHKpZsg/oIyRaKw54GKxZq5f7YivcWoZ8j7IQ65lHVH3gmaqKOvqdAVVt5imKZ +KQIhAPPsr9i3FxU+Mac0pvQKhFVJUzAFfKiG3ulVUdHgAaw/AiEA3ozHKzfZWKxH +gs8v8ZQ/FnfI7DwYYhJC0YsXb6NSvR8CIHymwLo73mTxsogjBQqDcVrwLL3GoAyz +V6jf+/8HvXMbAiEAj1b3FVQEboOQD6WoyJ1mQO9n/xf50HjYhqRitOnp6ZsCIQDS +AvkvYKc6LG8IANmVv93g1dyKZvU/OQkAZepqHZB2MQ== +-----END RSA PRIVATE KEY----- diff --git a/demo/rsa.priv0.pem b/demo/rsa.priv0.pem new file mode 100644 index 0000000..e2b93a1 --- /dev/null +++ b/demo/rsa.priv0.pem @@ -0,0 +1,15 @@ +-----BEGIN RSA PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: DES-CBC,244DDAFE6F991A53 + +h1/NnMxq2ku9UkBG2tfM7hCFWmOvwMbVQBYgS4jLm3ENvWRgkm7pCqjCug99Uo5o +r+NhzqyvSBH+eX3ojVAfpMxkL0TIjMrogRq2TD75v6hTGu/fd4Yrw7vZaKRbLzoh +rG6m7zdbiQwNyh0bTbbo3WmQ07FXkXrDqihLaZTJOvHNLS1lKwRjIS0MqtyhOfPj +NNwuNEs6AFz4k6UxNMRXhyU2SXn5SOgZZB12SCIsYA034rwKFKqNRoneaLSlhtut +1z/BlJaLiZDHJmtxtgz5h3Ss9fQ9J0pLnybx+EM70TPM3hz47/8cQiEYIVg7+QFW +jNYaGIA6IOcyB7fwX5e/zFy5yXAsmuIw/iP0sYa29hdUG++T8Mp2knrBBwQO77OS +WRz219dMQQRyyn2jpN5x23AZyz4gHj4YKMm3KO06Y2k= +-----END RSA PRIVATE KEY----- + +This is the same key as rsa.priv.pem. Passphrase is 'qwerty'. + diff --git a/demo/rsa.pub.pem b/demo/rsa.pub.pem new file mode 100644 index 0000000..42379aa --- /dev/null +++ b/demo/rsa.pub.pem @@ -0,0 +1,4 @@ +-----BEGIN PUBLIC KEY----- +MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBANQNY7RD9BarYRsmMazM1hd7a+u3QeMP +FZQ7Ic+BmmeWHvvVP4Yjyu1t6vAut7mKkaDeKbT3yiGVUgAEUaWMXqECAwEAAQ== +-----END PUBLIC KEY----- diff --git a/demo/rsa1024pvtkey.pem b/demo/rsa1024pvtkey.pem new file mode 100644 index 0000000..481f191 --- /dev/null +++ b/demo/rsa1024pvtkey.pem @@ -0,0 +1,15 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICXQIBAAKBgQDE3rnXDvA4lrIUj7ZXQM2ODLw0NnjKXh7+O3GPttvoFoacK1qk +BRO+aO4hPhvQumySTz+N2RkIsT9PTx6yfyWWwYC1zbbv2cxlCBisGAvrUSL6IfHX +ND2/vVN+QmOd/b4hfC/ekRo2ylrnX0pOXv/JxiblV9WumiZsBEuhUvxCnQIDAQAB +AoGBALdaLEDkM8ywZQiLVCptO0RaDgqe1N68zCbBXCGaD7NXD2VxZ0itRdcnyOiC +/MroZWfakPleQVd8JNeLe66IhosAJd7Zbjf/npYnD4zD1x54zRrSuBquXhz+ev7U +tZONPxU5QCf/aow4z1s/M+knLyy+0lAUlWwTgOrd5VCQ8suBAkEA5/v48HZW8pQC +2zxQUFfsnRu6DxJuQ1UvWp51FNUYZM2LjpwgGyCNkCw+nuxPgM1HnOTAMVXkZdwL +7XG0Gm5ZoQJBANlAKKUc7ItOfN5TeD154Qz9FLtZUgWnWfawvEqiHJ0cPrzyv9v6 +wu19QxP6ORZBWyr8c20/p2fNrk+7YOZAH30CQQDZ9p1HEWlQMlEcu+aaFoJyewKt +9pszGG6NriRDlpR84cMmEvr3gfaAZ5HOsCli031dpHAP6qvWKJHsXtDhpJ0BAkBs +fANP4A+myLzF8Hx8hl4BNGej3kh9FkJwU3TS9/y935rck4OG/8NTAFf8o9jZ6izy +XDnvdffMeALxQapzj9WpAkBQt2yjU3V3bKkNl0XwTk/VK8WGEWTBF1byZv/6VZOb +q/vOPSexrQmu+T8+lljraJISfIpMSWZhR4oarmnGVRPX +-----END RSA PRIVATE KEY----- diff --git a/demo/rsa_bench.py b/demo/rsa_bench.py new file mode 100644 index 0000000..b0adacc --- /dev/null +++ b/demo/rsa_bench.py @@ -0,0 +1,183 @@ +#!/usr/bin/env python + +""" + RSA signing demo and benchmark. + + Usage: python -O rsa_bench.py [option option option ...] + where options may include: + makenewkey showdigest showprofile + md5 sha1 sha256 sha512 + <key length> + + Larry Bugbee + November 2006 + + + Some portions are Copyright (c) 1999-2003 Ng Pheng Siong. + All rights reserved. + + Portions created by Open Source Applications Foundation + (OSAF) are Copyright (C) 2004 OSAF. All Rights Reserved. + +""" + +from M2Crypto import RSA, EVP, Rand +from M2Crypto.EVP import MessageDigest +import sys, base64 + +# -------------------------------------------------------------- +# program parameters + +makenewkey = 0 # 1 = make/save new key, 0 = use existing +showpubkey = 0 # 1 = show the public key value +showdigest = 0 # 1 = show the digest value +showprofile = 0 # 1 = use the python profiler + +hashalgs = ['md5', 'ripemd160', 'sha1', + 'sha224', 'sha256', 'sha384', 'sha512'] + +# default hashing algorithm +hashalg = 'sha1' + +# default key parameters +keylen = 1024 +exponent = 65537 +''' + There is some temptation to use an RSA exponent of 3 + because 1) it is easy to remember and 2) it minimizes the + effort of signature verification. Unfortunately there + a couple of attacks based on the use of 3. From a draft + RFC (Easklake, Dec 2000): + A public exponent of 3 minimizes the effort needed to + verify a signature. Use of 3 as the public exponent is + weak for confidentiality uses since, if the same data + can be collected encrypted under three different keys + with an exponent of 3 then, using the Chinese Remainder + Theorem [NETSEC], the original plain text can be easily + recovered. + This applies to confidentiality so it is not of major + concern here. The second attack is a protocol implementation + weakness and can be patched, but has the patch been applied? + ...correctly? It is arguably better to get into the habit + of using a stronger exponent and avoiding these and possible + future attacks based on 3. I suggest getting in the habit + of using something stronger. Some suggest using 65537. +''' + +# number of speed test loops +N1 = N2 = 100 + +# -------------------------------------------------------------- +# functions + +def test(rsa, dgst): + print ' testing signing and verification...', + try: + sig = rsa.sign(dgst) + except Exception, e: + print '\n\n *** %s *** \n' % e + sys.exit() + if not rsa.verify(dgst, sig): + print 'not ok' + else: + print 'ok' + +def test_asn1(rsa, dgst): + print ' testing asn1 signing and verification...', + blob = rsa.sign_asn1(dgst) + if not rsa.verify_asn1(dgst, blob): + print 'not ok' + else: + print 'ok' + +def speed(): + from time import time + t1 = time() + for i in range(N1): + sig = rsa.sign(dgst) + print ' %d signings: %8.2fs' % (N1, (time() - t1)) + t1 = time() + for i in range(N2): + rsa.verify(dgst, sig) + print ' %d verifications: %8.2fs' % (N2, (time() - t1)) + +def test_speed(rsa, dgst): + print ' measuring speed...' + if showprofile: + import profile + profile.run('speed()') + else: + speed() + print + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +def main(keylen, hashalg): + global rsa, dgst # this exists ONLY for speed testing + + Rand.load_file('randpool.dat', -1) + + pvtkeyfilename = 'rsa%dpvtkey.pem' % (keylen) + pubkeyfilename = 'rsa%dpubkey.pem' % (keylen) + + if makenewkey: + print ' making and saving a new key' + rsa = RSA.gen_key(keylen, exponent) + rsa.save_key(pvtkeyfilename, None ) # no pswd callback + rsa.save_pub_key(pubkeyfilename) + else: + print ' loading an existing key' + rsa = RSA.load_key(pvtkeyfilename) + print ' rsa key length:', len(rsa) + + if not rsa.check_key(): + raise 'key is not initialised' + + # since we are testing signing and verification, let's not + # be fussy about the digest. Just make one. + md = EVP.MessageDigest(hashalg) + md.update('can you spell subliminal channel?') + dgst = md.digest() + print ' hash algorithm: %s' % hashalg + if showdigest: + print ' %s digest: \n%s' % (hashalg, base64.encodestring(dgst)) + + test(rsa, dgst) +# test_asn1(rsa, dgst) + test_speed(rsa, dgst) + Rand.save_file('randpool.dat') + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +def print_usage(): + print """ + Usage: python -O %s [option option option ...] + where options may include: + makenewkey showdigest showprofile + md5 sha1 sha256 sha512 + <key length> +""" % sys.argv[0] + sys.exit() + +# -------------------------------------------------------------- +# -------------------------------------------------------------- + +if __name__=='__main__': + for arg in sys.argv[1:]: + if arg in hashalgs: hashalg = arg; continue + if arg == 'makenewkey': makenewkey = 1; continue + if arg == 'showpubkey': showpubkey = 1; continue + if arg == 'showdigest': showdigest = 1; continue + if arg == 'showprofile': showprofile = 1; continue + try: + keylen = int(arg) + except: + print '\n *** argument "%s" not understood ***' % arg + print_usage() + + main(keylen, hashalg) + + +# -------------------------------------------------------------- +# -------------------------------------------------------------- +# -------------------------------------------------------------- diff --git a/demo/rsatest.py b/demo/rsatest.py new file mode 100644 index 0000000..4277c4d --- /dev/null +++ b/demo/rsatest.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python + +"""RSA demonstration. + +Copyright (c) 1999-2003 Ng Pheng Siong. All rights reserved.""" + +from M2Crypto import RSA, EVP, Rand + +msg="The magic words are squeamish ossifrage." +sha1=EVP.MessageDigest('sha1') +sha1.update(msg) +dgst=sha1.digest() + +priv=RSA.load_key('rsa.priv.pem') +pub=RSA.load_pub_key('rsa.pub.pem') + +def test_encrypt(padding): + print 'testing public-key encryption:', padding + padding=eval('RSA.'+padding) + ctxt=pub.public_encrypt(dgst, padding) + ptxt=priv.private_decrypt(ctxt, padding) + if ptxt!=dgst: + print 'public_encrypt -> private_decrypt: not ok' + +def test_sign(padding): + print 'testing private-key signing:', padding + padding=eval('RSA.'+padding) + ctxt=priv.private_encrypt(dgst, padding) + ptxt=pub.public_decrypt(ctxt, padding) + if ptxt!=dgst: + print 'private_decrypt -> public_encrypt: not ok' + +def test0(): + print 'testing misc.' + print `pub.e`, `pub.n` + print `priv.e`, `priv.n` + +if __name__=='__main__': + Rand.load_file('randpool.dat', -1) + test_encrypt('pkcs1_padding') + test_encrypt('pkcs1_oaep_padding') + #test_encrypt('sslv23_padding') + test_sign('pkcs1_padding') + Rand.save_file('randpool.dat') + diff --git a/demo/smime.howto/README b/demo/smime.howto/README new file mode 100644 index 0000000..2b494d7 --- /dev/null +++ b/demo/smime.howto/README @@ -0,0 +1,5 @@ + 01 Apr 2001 +------------- + +These are the example programs from the HOWTO. + diff --git a/demo/smime.howto/decrypt.py b/demo/smime.howto/decrypt.py new file mode 100644 index 0000000..617f4ab --- /dev/null +++ b/demo/smime.howto/decrypt.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python + +"""S/MIME HOWTO demo program. + +Copyright (c) 1999-2001 Ng Pheng Siong. All rights reserved.""" + +from M2Crypto import BIO, SMIME, X509 + +# Instantiate an SMIME object. +s = SMIME.SMIME() + +# Load private key and cert. +s.load_key('recipient_key.pem', 'recipient.pem') + +# Load the encrypted data. +p7, data = SMIME.smime_load_pkcs7('encrypt.p7') + +# Decrypt p7. +out = s.decrypt(p7) + +print out + diff --git a/demo/smime.howto/dv.py b/demo/smime.howto/dv.py new file mode 100644 index 0000000..94223a8 --- /dev/null +++ b/demo/smime.howto/dv.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python + +"""S/MIME HOWTO demo program. + +Copyright (c) 1999-2001 Ng Pheng Siong. All rights reserved.""" + +from M2Crypto import BIO, SMIME, X509 + +# Instantiate an SMIME object. +s = SMIME.SMIME() + +# Load private key and cert. +s.load_key('recipient_key.pem', 'recipient.pem') + +# Load the signed/encrypted data. +p7, data = SMIME.smime_load_pkcs7('se.p7') + +# After the above step, 'data' == None. +# Decrypt p7. 'out' now contains a PKCS #7 signed blob. +out = s.decrypt(p7) + +# Load the signer's cert. +x509 = X509.load_cert('signer.pem') +sk = X509.X509_Stack() +sk.push(x509) +s.set_x509_stack(sk) + +# Load the signer's CA cert. In this case, because the signer's +# cert is self-signed, it is the signer's cert itself. +st = X509.X509_Store() +st.load_info('signer.pem') +s.set_x509_store(st) + +# Recall 'out' contains a PKCS #7 blob. +# Transform 'out'; verify the resulting PKCS #7 blob. +p7_bio = BIO.MemoryBuffer(out) +p7, data = SMIME.smime_load_pkcs7_bio(p7_bio) +v = s.verify(p7) + +print v + diff --git a/demo/smime.howto/encrypt.p7 b/demo/smime.howto/encrypt.p7 new file mode 100644 index 0000000..eb8b8a5 --- /dev/null +++ b/demo/smime.howto/encrypt.p7 @@ -0,0 +1,17 @@ +From: sender@example.dom +To: recipient@example.dom +MIME-Version: 1.0 +Content-Disposition: attachment; filename="smime.p7m" +Content-Type: application/x-pkcs7-mime; name="smime.p7m" +Content-Transfer-Encoding: base64 + +MIIBVwYJKoZIhvcNAQcDoIIBSDCCAUQCAQAxggEAMIH9AgEAMGYwYTELMAkGA1UE +BhMCU0cxETAPBgNVBAoTCE0yQ3J5cHRvMRkwFwYDVQQDExBTL01JTUUgUmVjaXBp +ZW50MSQwIgYJKoZIhvcNAQkBFhVyZWNpcGllbnRAZXhhbXBsZS5kb20CAQAwDQYJ +KoZIhvcNAQEBBQAEgYCdBULuQ/kfjgk17dMwBHdY+vXfqR/B+vJnF0gXhK6gc5d4 +gCkRefEyVu7lGAD2MOdD0cOLFvHdmGHFJqyJ8T+A1mmqrxMAiJImtqN586YnoTUb +Cw5FaMEd20d6hpGeEVBFHkam0vQKdR1GLG1VPiFn+ytB06g7NNphsI5/uFGyLDA7 +BgkqhkiG9w0BBwEwFAYIKoZIhvcNAwcECPfuW3hoPVr0gBiA4CiFG4m74CT4wHPu +q1fhEkG3zjraxn4= + + diff --git a/demo/smime.howto/encrypt.py b/demo/smime.howto/encrypt.py new file mode 100644 index 0000000..ae37526 --- /dev/null +++ b/demo/smime.howto/encrypt.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python + +"""S/MIME HOWTO demo program. + +Copyright (c) 1999-2001 Ng Pheng Siong. All rights reserved.""" + +from M2Crypto import BIO, Rand, SMIME, X509 + +def makebuf(text): + return BIO.MemoryBuffer(text) + +# Make a MemoryBuffer of the message. +buf = makebuf('a sign of our times') + +# Seed the PRNG. +Rand.load_file('randpool.dat', -1) + +# Instantiate an SMIME object. +s = SMIME.SMIME() + +# Load target cert to encrypt to. +x509 = X509.load_cert('recipient.pem') +sk = X509.X509_Stack() +sk.push(x509) +s.set_x509_stack(sk) + +# Set cipher: 3-key triple-DES in CBC mode. +s.set_cipher(SMIME.Cipher('des_ede3_cbc')) + +# Encrypt the buffer. +p7 = s.encrypt(buf) + +# Output p7 in mail-friendly format. +out = BIO.MemoryBuffer() +out.write('From: sender@example.dom\n') +out.write('To: recipient@example.dom\n') +out.write('Subject: M2Crypto S/MIME testing\n') +s.write(out, p7) + +print out.read() + +# Save the PRNG's state. +Rand.save_file('randpool.dat') + diff --git a/demo/smime.howto/recipient.pem b/demo/smime.howto/recipient.pem new file mode 100644 index 0000000..78b6c14 --- /dev/null +++ b/demo/smime.howto/recipient.pem @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE----- +MIIC9zCCAmCgAwIBAgIBADANBgkqhkiG9w0BAQQFADBhMQswCQYDVQQGEwJTRzER +MA8GA1UEChMITTJDcnlwdG8xGTAXBgNVBAMTEFMvTUlNRSBSZWNpcGllbnQxJDAi +BgkqhkiG9w0BCQEWFXJlY2lwaWVudEBleGFtcGxlLmRvbTAeFw0wMTAzMzExMTQy +MTVaFw0wMjAzMzExMTQyMTVaMGExCzAJBgNVBAYTAlNHMREwDwYDVQQKEwhNMkNy +eXB0bzEZMBcGA1UEAxMQUy9NSU1FIFJlY2lwaWVudDEkMCIGCSqGSIb3DQEJARYV +cmVjaXBpZW50QGV4YW1wbGUuZG9tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKB +gQDQwLwQSshbTn/GUZXnZUQEUDc61OUd+qcPpHfi76Y7ar+2NwsalQ3bu2i7edEK +IZZWMFRnrOwE9PhmJHJIzfYDswgpHWtRy0P/Oyzt5kLBjvJYuMIqu8gZtWFz0G28 +Q8tGvIuPdWba+9TT3LOv4CXNF1V0k0KgAPd1Uq2FUcBa2QIDAQABo4G+MIG7MB0G +A1UdDgQWBBQe7b4CDEBuMJyiscil27YBdZBr9zCBiwYDVR0jBIGDMIGAgBQe7b4C +DEBuMJyiscil27YBdZBr96FlpGMwYTELMAkGA1UEBhMCU0cxETAPBgNVBAoTCE0y +Q3J5cHRvMRkwFwYDVQQDExBTL01JTUUgUmVjaXBpZW50MSQwIgYJKoZIhvcNAQkB +FhVyZWNpcGllbnRAZXhhbXBsZS5kb22CAQAwDAYDVR0TBAUwAwEB/zANBgkqhkiG +9w0BAQQFAAOBgQAJbQXzP7AK9u2kyvl8oeZAeJQDRsip4PVMkYd64HW3Hq/9ud3g +hj/laeUyfcga+c1c1yPUug5Ab+loeHhEEKsL9LqYFXzpFU1lXaID02zcqG7g3PWe +r9RKsUqrn4ZbRQ+clidnIx4nYLuG6CPQ6ME/uFrYHMsmQEO/+KoJONf/cg== +-----END CERTIFICATE----- diff --git a/demo/smime.howto/recipient_key.pem b/demo/smime.howto/recipient_key.pem new file mode 100644 index 0000000..0cb61f6 --- /dev/null +++ b/demo/smime.howto/recipient_key.pem @@ -0,0 +1,15 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICXAIBAAKBgQDQwLwQSshbTn/GUZXnZUQEUDc61OUd+qcPpHfi76Y7ar+2Nwsa +lQ3bu2i7edEKIZZWMFRnrOwE9PhmJHJIzfYDswgpHWtRy0P/Oyzt5kLBjvJYuMIq +u8gZtWFz0G28Q8tGvIuPdWba+9TT3LOv4CXNF1V0k0KgAPd1Uq2FUcBa2QIDAQAB +AoGAVOMrFozycH65YtHmXVRGlmJwMxJDoS8+JBRDVBsTw/Gix9wWPdcC7amF60ac +BLynv6Cjkg01ZMahBBgqCQUH1rii6Kg20MJJtpqvt1X+CAZkytVsQqwutSQXHj+g +TzZVDxQiuPKMyVhKTSVqutqs2EyFgSKcYuodfms5xDk2EyECQQD6vnEAl2PHBoia +5wrauujbWTM6H5oioWvJgLaUNgUhJ86/Y+ewKoGxLdYaxx99KhKxN/04i2chIHk0 +c53THOt9AkEA1SD1Rdm93FUMEor+BYEQgiN/4pWnSIsgUjyqV7lPv9QegdDTbVfm +WuPNev6Z+qo9mpDWbvhCZhH159q7uGfzjQJAe88dLRWThuqK+TGsAmTYJbbdvI1u +JjteZZjQjk4+KijlxUsnU60pbLsdRQudWMg1gpwKxKjQu2K1dljATUWyYQJARI83 +l2K1+py5J3XixS6BevukdeUiTOnEWe/98/4+szyvG59rg+8UwQQq43fnXIVLD9+r +u0LNSTxZ2F26qVV3OQJBAIBG0Gv9C44UlCPiJhmMqcpzexX20erOEGu+UiCUhHAC +ZdWdFaD2dlmk0O/E82LxPPivkGv5DtkNpzCl+3Vo+kI= +-----END RSA PRIVATE KEY----- diff --git a/demo/smime.howto/se.p7 b/demo/smime.howto/se.p7 new file mode 100644 index 0000000..df4af0f --- /dev/null +++ b/demo/smime.howto/se.p7 @@ -0,0 +1,56 @@ +From: sender@example.dom +To: recipient@example.dom +MIME-Version: 1.0 +Content-Disposition: attachment; filename="smime.p7m" +Content-Type: application/x-pkcs7-mime; name="smime.p7m" +Content-Transfer-Encoding: base64 + +MIIIwwYJKoZIhvcNAQcDoIIItDCCCLACAQAxggEAMIH9AgEAMGYwYTELMAkGA1UE +BhMCU0cxETAPBgNVBAoTCE0yQ3J5cHRvMRkwFwYDVQQDExBTL01JTUUgUmVjaXBp +ZW50MSQwIgYJKoZIhvcNAQkBFhVyZWNpcGllbnRAZXhhbXBsZS5kb20CAQAwDQYJ +KoZIhvcNAQEBBQAEgYCn4qL7Z8QlmVEMbO2zU5RUtBqw1cYPzxd/4ZF9H+djlL7f +EDIYHidvTBksXwcBXSTxsKsSusGF95NtwaZf4D3eiR1ChdkeXaiWCUPAQr0Mw4+A +EJa9eGBuLH+LXH0pDN5OhYb8hsL/EcV2OpOEjOe4aM/u3Ot5u1kK1582oikhCDCC +B6UGCSqGSIb3DQEHATAUBggqhkiG9w0DBwQImrxQEPwEHHeAggeACeVVKqKjySva +zos9ahumXC7bMIcfesxQF0L2CcR4wQtnwuzESXNAnQabtlN6cXKLNFGWmilztaEZ +zHFEEDqtqEeP5BmEkjnNFakhr49yrK/SZpVYYScsUHNcMDOwXMPKKxkk3qOKUlqQ +m+WvvyaWwZ8kbZNkO+YY4BpOVDEQRH3HG7ngtLP3y/LSx7kcpTl0wi8On9pigZ46 +4u9Odac7bNh5FVMOFiXbIXMuFJ1epy9agtX09eOK9JC9ZHnIl9IgGEL833d+l8MQ +OdeKqvSTnNf0JKZs52uzUqluI4pywNIiJT1OeU3Ch2vdfvcOcTQohGhIEUb7NWEn +Z02pBiZ0Tzg/T/sr8B60rPcRSSnGYAfQZPS86QgAMIEaqoHQVox+Qgigr2dynvWQ +KO0ny4iJLhtRoxD7W7LxaZat6fWvbC4WXKwgis2UJnEWwe8DsoZ3XEnYid3ke2/F +2+ejHtugd6E4o05vRAO/pOVZUbc4mbZm2zoPgzwm0M9GqQcTOVy1tkpEc6mhrH6y +AjbSeP/1YkZakDB9bl+TCw24lRquIUbGyNI9GM+oFYyciIYu6RfPhfoh9YU9wqS7 +KcnHk3WG5AxJZ6tZAsz/i/DhtD5zxFsldKMy5OaeBjh6txmQ0aHXPYLmIEdxPSa0 +BnrAeTam/1iOboXT1O2GkPiLJAJjWRAvK/QXsxJMGtSYEnwT+kkAZtU75FOyBuxI +gQOHO98CGGEy5CI/e/Th+1yMqyiimqDmfVktN4KgzZXbjyv4xzpgNE6o85v7fUg1 +1WoTF38GmbcIbVfBHaRwdWmSxeAsFltHb38u7pP8D24+3hC5OKsNLXxGFJhfp3HW +4A51fNAi3CaWIh9aZb+SYof8MmtZp9mjL0dKB1OZTyTXqkQ/yU3PIHE+ytM6men3 +tzQefsJlnk/t/zBhxdZo+yCna/ZJe3KmrShoD4ob9prWs48ohDhG7cY4vfW0NuxX +Im30y+JZe3A0Ktamq1VmpWDhW6Emo+Wk04nDTtU2AcClMoKWgcvIUp0HAgWDhrrx +6Rf1CiRZngJuCEheCMAvKT8IgcTt7vWRUpRoJrhlKR23GKEVA6T6DF9q8L4F82ul +yzQ2kdec6s8bD1EQT7e0QcIqEn8RNoSa2aLxRG2JBWqv0AZflKyCjwx35mCv7AmN +17zPENkGdDgE9xcEEdEe4rkigJ8uaYauEofQsDgSHWQ2oAk91g5FLNImsRn3Z7uy +qtjc9JTXOymowDXbdYebqNZTxJhXDUnrbawWqyCgohtytnVdW61wKsXV3oEp7PZc +aL0ZZvVkVkUofaf/2gIGvQ9WnTfpUJ/bEAr0hDdtnmg1QPVu/v/l9eXAnbkh71GX +OdFpIfvf0zVm/tn/7Vi+HfHRjupHSJBO+bIVQBt3uMLlovU48axSRXUnpIXdHYwN +3b/MabDQnzV1qvhGTRRj+o39RbDrueNSpXIleA7SzFqZO2LnaDZ/FKbca1CAL+Iy +KsByJUz7cReE4gU8N3SKKv7ivaibmFG63cvwA1GMUsWZgIYD/xy2nCB4jPCodiFB +wWUgAblxMcaidKox6Z1zK9oZA62QIX4XGl+0mE/xwxmt2fE5lSBhY8q7N7MvS/Nv +RDLEgULBMRvcjgIwL4MZZYkUl7oAqQ2aHL45j7vp7U9vIK2j8bcp9v5K3jeccENJ +qYn/35awflQ66lrrSUWH3evs5IO0FhKSn9c1GGTaZDA5TABzJKySmvphyAh1m24W +ylzgXQkp/Zi2oWajZ50txiIBgB14dWhuCS4vcwkWjyKur/yGpkY5DILSHZyguqDk +cwOgKRrC5myL7Zr0/5CwN6sBofOGb2Qhcx+4Cg6DnPwyMXKVTKYnXPvcTj2KvVW+ +qzTIhur0rwC13lK1+sMs/+Esel99fohCTz7+tiebqkPaFcnx8hUgMrllYfGCMXQ/ +hiY+1BVEyDWHWOjj5lxPwzNtPqbVz3uA//Rut/BozOpD/hEjCMVRbadSFZTM+x6+ +nr39DHGZsOn3XSEZSU2C93v77Ls/vdJi6IdJTwCD/yoEK1OY60ciBvCpJ/lLVSQl +PAdozKEAjqP+fX95MeAENtysXTsqbWawWeS7H/zXmwpWn3LnSJ48pymvwV6nJrYL +YXHaiuMGIZim9jOT04AyK5LPZ2E9omwVFaHgi10iRigc9y5MixHvN9P5WQ9nP+/c +kflFQXuph3RqBEWfG8vPFKFvqVQTN0lxUXoOXkKhZ7/Doe3orbfRw/xZUyKnfrzs +O7MU7PVAubVNUXjKHs2UCYtVMUH0tSuDMkuWUd6d6cmkPht86KvopoXha6zRw1pu +SE/nGq/o6bproWMPRfoCCN+xow9jeipTD7xkYbiw1+g/9hYaqUYm7QUbuYOrBtLe +RAlm69yornWrh7+n3GXUSCFQz3nj6sE4JjXciQzhUfEFrCn+qDrgaviFKe0yVYbQ +LRr3qtSM0gTVWn2XnOlnNuLdvx2UWV7KNNPrnRIoK3KvnfOJTl/RzkT1g+HaEvC3 +MFNlSJNXN6Gavdr8pAdtnVGivdnM1iw0yFKuuzoF1F+cuN8rGMmp + + diff --git a/demo/smime.howto/se.py b/demo/smime.howto/se.py new file mode 100644 index 0000000..cd4df60 --- /dev/null +++ b/demo/smime.howto/se.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python + +"""S/MIME HOWTO demo program. + +Copyright (c) 1999-2001 Ng Pheng Siong. All rights reserved.""" + +from M2Crypto import BIO, Rand, SMIME, X509 + +def makebuf(text): + return BIO.MemoryBuffer(text) + +# Make a MemoryBuffer of the message. +buf = makebuf('a sign of our times') + +# Seed the PRNG. +Rand.load_file('randpool.dat', -1) + +# Instantiate an SMIME object. +s = SMIME.SMIME() + +# Load signer's key and cert. Sign the buffer. +s.load_key('signer_key.pem', 'signer.pem') +p7 = s.sign(buf) + +# Load target cert to encrypt the signed message to. +x509 = X509.load_cert('recipient.pem') +sk = X509.X509_Stack() +sk.push(x509) +s.set_x509_stack(sk) + +# Set cipher: 3-key triple-DES in CBC mode. +s.set_cipher(SMIME.Cipher('des_ede3_cbc')) + +# Create a temporary buffer. +tmp = BIO.MemoryBuffer() + +# Write the signed message into the temporary buffer. +s.write(tmp, p7) + +# Encrypt the temporary buffer. +p7 = s.encrypt(tmp) + +# Output p7 in mail-friendly format. +out = BIO.MemoryBuffer() +out.write('From: sender@example.dom\n') +out.write('To: recipient@example.dom\n') +out.write('Subject: M2Crypto S/MIME testing\n') +s.write(out, p7) + +print out.read() + +# Save the PRNG's state. +Rand.save_file('randpool.dat') + diff --git a/demo/smime.howto/sendsmime.py b/demo/smime.howto/sendsmime.py new file mode 100644 index 0000000..6df9a89 --- /dev/null +++ b/demo/smime.howto/sendsmime.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python + +"""S/MIME sender. + +Copyright (c) 1999-2001 Ng Pheng Siong. All rights reserved.""" + +from M2Crypto import BIO, Rand, SMIME, X509 +import smtplib, string, sys + +def sendsmime(from_addr, to_addrs, subject, msg, from_key, from_cert=None, to_certs=None, smtpd='localhost'): + + msg_bio = BIO.MemoryBuffer(msg) + sign = from_key + encrypt = to_certs + + s = SMIME.SMIME() + if sign: + s.load_key(from_key, from_cert) + p7 = s.sign(msg_bio, flags=SMIME.PKCS7_TEXT) + msg_bio = BIO.MemoryBuffer(msg) # Recreate coz sign() has consumed it. + + if encrypt: + sk = X509.X509_Stack() + for x in to_certs: + sk.push(X509.load_cert(x)) + s.set_x509_stack(sk) + s.set_cipher(SMIME.Cipher('rc2_40_cbc')) + tmp_bio = BIO.MemoryBuffer() + if sign: + s.write(tmp_bio, p7) + else: + tmp_bio.write(msg) + p7 = s.encrypt(tmp_bio) + + out = BIO.MemoryBuffer() + out.write('From: %s\r\n' % from_addr) + out.write('To: %s\r\n' % string.join(to_addrs, ", ")) + out.write('Subject: %s\r\n' % subject) + if encrypt: + s.write(out, p7) + else: + if sign: + s.write(out, p7, msg_bio, SMIME.PKCS7_TEXT) + else: + out.write('\r\n') + out.write(msg) + out.close() + + smtp = smtplib.SMTP() + smtp.connect(smtpd) + smtp.sendmail(from_addr, to_addrs, out.read()) + smtp.quit() + + # XXX Cleanup the stack and store. + + +msg = """ +S/MIME - Secure Multipurpose Internet Mail Extensions [RFC 2311, RFC 2312] - +provides a consistent way to send and receive secure MIME data. Based on the +popular Internet MIME standard, S/MIME provides the following cryptographic +security services for electronic messaging applications - authentication, +message integrity and non-repudiation of origin (using digital signatures) +and privacy and data security (using encryption). + +S/MIME is built on the PKCS #7 standard. [PKCS7] + +S/MIME is implemented in Netscape Messenger and Microsoft Outlook. +""" + + +if __name__ == '__main__': + Rand.load_file('../randpool.dat', -1) + sendsmime(from_addr = 'ngps@post1.com', + to_addrs = ['popuser@nova.dyndns.org'], + subject = 'S/MIME testing', + msg = msg, + #from_key = 'signer.pem', + from_key = None, + #to_certs = None) + to_certs = ['recipient.pem']) + Rand.save_file('../randpool.dat') + diff --git a/demo/smime.howto/sign.p7 b/demo/smime.howto/sign.p7 new file mode 100644 index 0000000..7a39eaa --- /dev/null +++ b/demo/smime.howto/sign.p7 @@ -0,0 +1,46 @@ +From: sender@example.dom +To: recipient@example.dom +Subject: M2Crypto S/MIME testing +MIME-Version: 1.0 +Content-Type: multipart/signed ; protocol="application/x-pkcs7-signature" ; micalg=sha1 ; boundary="----2221986D060512556B8AC18AAA106F39" + +This is an S/MIME signed message + +------2221986D060512556B8AC18AAA106F39 +a sign of our times +------2221986D060512556B8AC18AAA106F39 +Content-Type: application/x-pkcs7-signature; name="smime.p7s" +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; filename="smime.p7s" + +MIIE8AYJKoZIhvcNAQcCoIIE4TCCBN0CAQExCzAJBgUrDgMCGgUAMCIGCSqGSIb3 +DQEHAaAVBBNhIHNpZ24gb2Ygb3VyIHRpbWVzoIIC5zCCAuMwggJMoAMCAQICAQAw +DQYJKoZIhvcNAQEEBQAwWzELMAkGA1UEBhMCU0cxETAPBgNVBAoTCE0yQ3J5cHRv +MRYwFAYDVQQDEw1TL01JTUUgU2VuZGVyMSEwHwYJKoZIhvcNAQkBFhJzZW5kZXJA +ZXhhbXBsZS5kb20wHhcNMDEwMzMxMTE0MDMzWhcNMDIwMzMxMTE0MDMzWjBbMQsw +CQYDVQQGEwJTRzERMA8GA1UEChMITTJDcnlwdG8xFjAUBgNVBAMTDVMvTUlNRSBT +ZW5kZXIxITAfBgkqhkiG9w0BCQEWEnNlbmRlckBleGFtcGxlLmRvbTCBnzANBgkq +hkiG9w0BAQEFAAOBjQAwgYkCgYEA5c5Tj1CHTSOxa1q2q0FYiwMWYHptJpJcvtZm +UwrgU5sHrA8OnCM0cDXEj0KPf3cfNjHffB8HWMzI4UEgNmFXQNsxoGZ+iqwxLlNj +y9Mh7eFW/Bjq5hNXbouSlQ0rWBRkoxV64y+t6lQehb32WfYXQbKFxFJSXzSxOx3R +8YhSPd0CAwEAAaOBtjCBszAdBgNVHQ4EFgQUXOyolL1t4jaBwZFRM7MS8nBLzUow +gYMGA1UdIwR8MHqAFFzsqJS9beI2gcGRUTOzEvJwS81KoV+kXTBbMQswCQYDVQQG +EwJTRzERMA8GA1UEChMITTJDcnlwdG8xFjAUBgNVBAMTDVMvTUlNRSBTZW5kZXIx +ITAfBgkqhkiG9w0BCQEWEnNlbmRlckBleGFtcGxlLmRvbYIBADAMBgNVHRMEBTAD +AQH/MA0GCSqGSIb3DQEBBAUAA4GBAHo3DrCHR86fSTVAvfiXdSswWqKtCEhUHRdC +TLFGl4hDk2GyZxaFuqZwiURz/H7nMicymI2wkz8H/wyHFg8G3BIehURpj2v/ZWXY +eovbgS7EZALVVkDj4hNl/IIHWd6Gtv1UODf7URbxtl3hQ9/eTWITrefT1heuPnar +8czydsOLMYIBujCCAbYCAQEwYDBbMQswCQYDVQQGEwJTRzERMA8GA1UEChMITTJD +cnlwdG8xFjAUBgNVBAMTDVMvTUlNRSBTZW5kZXIxITAfBgkqhkiG9w0BCQEWEnNl +bmRlckBleGFtcGxlLmRvbQIBADAJBgUrDgMCGgUAoIGxMBgGCSqGSIb3DQEJAzEL +BgkqhkiG9w0BBwEwHAYJKoZIhvcNAQkFMQ8XDTAxMDMzMTE0NTgyMVowIwYJKoZI +hvcNAQkEMRYEFOoeRUd8ExIYXfQq8BTFuKWrSP3iMFIGCSqGSIb3DQEJDzFFMEMw +CgYIKoZIhvcNAwcwDgYIKoZIhvcNAwICAgCAMA0GCCqGSIb3DQMCAgFAMAcGBSsO +AwIHMA0GCCqGSIb3DQMCAgEoMA0GCSqGSIb3DQEBAQUABIGAKMjEdSteeZg9ji1C +Y2r6zuxDVVftpSLy9ZhYOblGYY2b3PhzI0XytdUoLdb+GlPImqE/F5FJCPvTAovq +uLElbx/FuijA2ly7PQF2ekyFJ0EhvNWT1gcLhi1m0W+Hk/jgCQx7YRojT53Pa9fj +zuHk7ZkSKe5rZlOJvPJtYREnvD8= + +------2221986D060512556B8AC18AAA106F39-- + + diff --git a/demo/smime.howto/sign.py b/demo/smime.howto/sign.py new file mode 100644 index 0000000..38c4bab --- /dev/null +++ b/demo/smime.howto/sign.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python + +"""S/MIME HOWTO demo program. + +Copyright (c) 1999-2001 Ng Pheng Siong. All rights reserved.""" + +from M2Crypto import BIO, Rand, SMIME + +def makebuf(text): + return BIO.MemoryBuffer(text) + +# Make a MemoryBuffer of the message. +buf = makebuf('a sign of our times') + +# Seed the PRNG. +Rand.load_file('randpool.dat', -1) + +# Instantiate an SMIME object; set it up; sign the buffer. +s = SMIME.SMIME() +s.load_key('signer_key.pem', 'signer.pem') +p7 = s.sign(buf) + +# Recreate buf. +buf = makebuf('a sign of our times') + +# Output p7 in mail-friendly format. +out = BIO.MemoryBuffer() +out.write('From: sender@example.dom\n') +out.write('To: recipient@example.dom\n') +out.write('Subject: M2Crypto S/MIME testing\n') +s.write(out, p7, buf) + +print out.read() + +# Save the PRNG's state. +Rand.save_file('randpool.dat') + diff --git a/demo/smime.howto/signer.pem b/demo/smime.howto/signer.pem new file mode 100644 index 0000000..9189aaa --- /dev/null +++ b/demo/smime.howto/signer.pem @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE----- +MIIC4zCCAkygAwIBAgIBADANBgkqhkiG9w0BAQQFADBbMQswCQYDVQQGEwJTRzER +MA8GA1UEChMITTJDcnlwdG8xFjAUBgNVBAMTDVMvTUlNRSBTZW5kZXIxITAfBgkq +hkiG9w0BCQEWEnNlbmRlckBleGFtcGxlLmRvbTAeFw0wMTAzMzExMTQwMzNaFw0w +MjAzMzExMTQwMzNaMFsxCzAJBgNVBAYTAlNHMREwDwYDVQQKEwhNMkNyeXB0bzEW +MBQGA1UEAxMNUy9NSU1FIFNlbmRlcjEhMB8GCSqGSIb3DQEJARYSc2VuZGVyQGV4 +YW1wbGUuZG9tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDlzlOPUIdNI7Fr +WrarQViLAxZgem0mkly+1mZTCuBTmwesDw6cIzRwNcSPQo9/dx82Md98HwdYzMjh +QSA2YVdA2zGgZn6KrDEuU2PL0yHt4Vb8GOrmE1dui5KVDStYFGSjFXrjL63qVB6F +vfZZ9hdBsoXEUlJfNLE7HdHxiFI93QIDAQABo4G2MIGzMB0GA1UdDgQWBBRc7KiU +vW3iNoHBkVEzsxLycEvNSjCBgwYDVR0jBHwweoAUXOyolL1t4jaBwZFRM7MS8nBL +zUqhX6RdMFsxCzAJBgNVBAYTAlNHMREwDwYDVQQKEwhNMkNyeXB0bzEWMBQGA1UE +AxMNUy9NSU1FIFNlbmRlcjEhMB8GCSqGSIb3DQEJARYSc2VuZGVyQGV4YW1wbGUu +ZG9tggEAMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEEBQADgYEAejcOsIdHzp9J +NUC9+Jd1KzBaoq0ISFQdF0JMsUaXiEOTYbJnFoW6pnCJRHP8fucyJzKYjbCTPwf/ +DIcWDwbcEh6FRGmPa/9lZdh6i9uBLsRkAtVWQOPiE2X8ggdZ3oa2/VQ4N/tRFvG2 +XeFD395NYhOt59PWF64+dqvxzPJ2w4s= +-----END CERTIFICATE----- diff --git a/demo/smime.howto/signer_key.pem b/demo/smime.howto/signer_key.pem new file mode 100644 index 0000000..ad15a87 --- /dev/null +++ b/demo/smime.howto/signer_key.pem @@ -0,0 +1,15 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICXgIBAAKBgQDlzlOPUIdNI7FrWrarQViLAxZgem0mkly+1mZTCuBTmwesDw6c +IzRwNcSPQo9/dx82Md98HwdYzMjhQSA2YVdA2zGgZn6KrDEuU2PL0yHt4Vb8GOrm +E1dui5KVDStYFGSjFXrjL63qVB6FvfZZ9hdBsoXEUlJfNLE7HdHxiFI93QIDAQAB +AoGBALqc4OgZSbYPjQyTfpD1IJTKLgqsgCR5aE0kR7WZuG7MDt/e3ktWn0ebsgFv +2J12u2bD+yqM++dVbK7WtvTR+QpMhb/0XMhXNsvmn5gOLdKlJjS0RXDDs2DzIS6p +JNzAmn5zqTVteZAMDLk7ygkO++iGzwRz713ZgxRaKr5YWiLVAkEA+3ev1TTXNEOk +wQ9fbukMrfUXesqwgrx9VZ1z1X5we42RIIMTYI1edpcujXYvgS3/jdzAWDS1Nqta +9QB3uy91ywJBAOnysIIQhHn+4zvaOA5vh85WczPhN9C+yRmV70eyL9h+aThUFS4c +kg2jQOLp8MaxAkmk4xRbZBgehjmDr45b5fcCQQDpIGNlYFBmhpN129+YffugRgjX +cJNFEKONPKRHd6mmEW9K2dmb+FNr0+p3gOq3csJpbQ7wdyTMov13B1D4ux4TAkAR +URB1oCleKlrBjGaH0wOXZ1jBp1MNVYHnLez3Pp5CBSFetQKYVi8NaV8dLLnQyztj +Hhxc3mLrUh8XVMMC45SDAkEAxRCKmkneLceIdwixLIUF0zr0OzJtgyhxMxvHu3ET +gJcZqNN0y3EgPwcNihpBw7rjpp5e5sjlRNVqLqn8a5/Fog== +-----END RSA PRIVATE KEY----- diff --git a/demo/smime.howto/verify.py b/demo/smime.howto/verify.py new file mode 100644 index 0000000..9430add --- /dev/null +++ b/demo/smime.howto/verify.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python + +"""S/MIME HOWTO demo program. + +Copyright (c) 1999-2001 Ng Pheng Siong. All rights reserved.""" + +from M2Crypto import SMIME, X509 + +# Instantiate an SMIME object. +s = SMIME.SMIME() + +# Load the signer's cert. +x509 = X509.load_cert('signer.pem') +sk = X509.X509_Stack() +sk.push(x509) +s.set_x509_stack(sk) + +# Load the signer's CA cert. In this case, because the signer's +# cert is self-signed, it is the signer's cert itself. +st = X509.X509_Store() +st.load_info('signer.pem') +s.set_x509_store(st) + +# Load the data, verify it. +p7, data = SMIME.smime_load_pkcs7('sign.p7') +v = s.verify(p7) +print v +print data +print data.read() + diff --git a/demo/smime/README b/demo/smime/README new file mode 100644 index 0000000..736e632 --- /dev/null +++ b/demo/smime/README @@ -0,0 +1,54 @@ + 29 Nov 2000 +------------- +- test.py works. + +- makesmime.py is sendsmime.py modified to not send + SMTP, because I do not now have an SMTP server handy. + +- sendsmime.py should still work, because makesmime.py + does. + +- unsmime.py doesn't work, possibly because the certificates + used to generate the tested PKCS7 objects are no longer + available. + + + + 20 Nov 2000 +------------- + +This directory contains various programs and supporting files +demonstrating M2Crypto's S/MIME functionality. + +- test.py exercises the various S/MIME functionality. +- sendsmime.py (optionally) signs and/or encrypts a message, + then sends the output via SMTP. +- makesmime.py is exactly like sendsmime.py, except it writes + its output to sys.stdout. +- unsmime.py decrypts and verifies an S/MIME SignedAndEnveloped + message. It handles the S/MIME output of Netscape Messenger + successfully. + +- ca.pem is M2Crypto's test CA certificate. +- client.pem and client2.pem contain user certificates and their + corresponding private keys. + +- clear.p7 is a clear-signed S/MIME message. +- opaque.p7 is a signed S/MIME message. +- ns.p7 is a clear-signed S/MIME message produced by Messenger. +- ns.se.p7 is a signed-then-encrypted S/MIME message produced + by Messenger. +- m2.se.p7 is a signed-then-encrypted S/MIME message produced + by sendsmime.py. + + +I tested with export and strong versions of Netscape Communicator +4.7x. + +I have also done some interoperability testing with Sampo Kellomaki's +smime tool. + +I am interested in interoperability testing with Outlook and other +S/MIME tools. Write me <ngps@post1.com> if you want to collaborate. + + diff --git a/demo/smime/ca.pem b/demo/smime/ca.pem new file mode 100644 index 0000000..b7c84a1 --- /dev/null +++ b/demo/smime/ca.pem @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDWTCCAsKgAwIBAgIBADANBgkqhkiG9w0BAQQFADCBgDELMAkGA1UEBhMCU0cx +ETAPBgNVBAoTCE0yQ3J5cHRvMRQwEgYDVQQLEwtNMkNyeXB0byBDQTEkMCIGA1UE +AxMbTTJDcnlwdG8gQ2VydGlmaWNhdGUgTWFzdGVyMSIwIAYJKoZIhvcNAQkBFhNu +Z3BzQG5ldG1lbWV0aWMuY29tMB4XDTAxMTIxNTA1NTU0NloXDTA0MTIxNDA1NTU0 +NlowgYAxCzAJBgNVBAYTAlNHMREwDwYDVQQKEwhNMkNyeXB0bzEUMBIGA1UECxML +TTJDcnlwdG8gQ0ExJDAiBgNVBAMTG00yQ3J5cHRvIENlcnRpZmljYXRlIE1hc3Rl +cjEiMCAGCSqGSIb3DQEJARYTbmdwc0BuZXRtZW1ldGljLmNvbTCBnzANBgkqhkiG +9w0BAQEFAAOBjQAwgYkCgYEAx8soJbS719LHK62VVVIQeC3oW0HvFArwPnA0LuEK +q+LaqMOJg1rS7hvFdX03diV+XJw7cC0iECZYJNG4ii1xbY6KRmufkInaAwm54E3N +e+YYVocaqUkcN6xVf6fwnLfPXbpFS/K2Umg11ObKMmi80JmiIdjcjRRCQZC7g1hf +q+kCAwEAAaOB4DCB3TAdBgNVHQ4EFgQU6/qcBzEtQphfXLhiOHbt2KqBwMIwga0G +A1UdIwSBpTCBooAU6/qcBzEtQphfXLhiOHbt2KqBwMKhgYakgYMwgYAxCzAJBgNV +BAYTAlNHMREwDwYDVQQKEwhNMkNyeXB0bzEUMBIGA1UECxMLTTJDcnlwdG8gQ0Ex +JDAiBgNVBAMTG00yQ3J5cHRvIENlcnRpZmljYXRlIE1hc3RlcjEiMCAGCSqGSIb3 +DQEJARYTbmdwc0BuZXRtZW1ldGljLmNvbYIBADAMBgNVHRMEBTADAQH/MA0GCSqG +SIb3DQEBBAUAA4GBAD+I14GuS5vJmyv1k7mUMbAicsWRHZ+zrGOq9L/L2LsA+lKQ +dAzEZE2+Zv8LBPJVltbJJhcFNJS/ZMAjEm4xlJuCpvXVMxd/M5AM29aqekWlIK7J +vsdDL8IuzpRkMniUiNKPhmB6IPIOslvUKx6QofcE0wDh6pg4VvIbCjkpZ7gf +-----END CERTIFICATE----- diff --git a/demo/smime/clear.p7 b/demo/smime/clear.p7 new file mode 100644 index 0000000..c878407 --- /dev/null +++ b/demo/smime/clear.p7 @@ -0,0 +1,67 @@ +To: ngps@post1.com +From: ngps@post1.com +Subject: testing +MIME-Version: 1.0 +Content-Type: multipart/signed; protocol="application/x-pkcs7-signature"; micalg=sha1; boundary="----F0DD27AD8CDFF9AB55B87EE3A626E6D7" + +This is an S/MIME signed message + +------F0DD27AD8CDFF9AB55B87EE3A626E6D7 + +S/MIME - Secure Multipurpose Internet Mail Extensions [RFC 2311, RFC 2312] - +provides a consistent way to send and receive secure MIME data. Based on the +popular Internet MIME standard, S/MIME provides the following cryptographic +security services for electronic messaging applications - authentication, +message integrity and non-repudiation of origin (using digital signatures) +and privacy and data security (using encryption). + +S/MIME is built on the PKCS #7 standard. [PKCS7] + +S/MIME is implemented in Netscape Messenger and Microsoft Outlook. + +------F0DD27AD8CDFF9AB55B87EE3A626E6D7 +Content-Type: application/x-pkcs7-signature; name="smime.p7s" +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; filename="smime.p7s" + +MIIHHAYJKoZIhvcNAQcCoIIHDTCCBwkCAQExCzAJBgUrDgMCGgUAMIICQwYJKoZI +hvcNAQcBoIICNASCAjANClMvTUlNRSAtIFNlY3VyZSBNdWx0aXB1cnBvc2UgSW50 +ZXJuZXQgTWFpbCBFeHRlbnNpb25zIFtSRkMgMjMxMSwgUkZDIDIzMTJdIC0gDQpw +cm92aWRlcyBhIGNvbnNpc3RlbnQgd2F5IHRvIHNlbmQgYW5kIHJlY2VpdmUgc2Vj +dXJlIE1JTUUgZGF0YS4gQmFzZWQgb24gdGhlDQpwb3B1bGFyIEludGVybmV0IE1J +TUUgc3RhbmRhcmQsIFMvTUlNRSBwcm92aWRlcyB0aGUgZm9sbG93aW5nIGNyeXB0 +b2dyYXBoaWMNCnNlY3VyaXR5IHNlcnZpY2VzIGZvciBlbGVjdHJvbmljIG1lc3Nh +Z2luZyBhcHBsaWNhdGlvbnMgLSBhdXRoZW50aWNhdGlvbiwNCm1lc3NhZ2UgaW50 +ZWdyaXR5IGFuZCBub24tcmVwdWRpYXRpb24gb2Ygb3JpZ2luICh1c2luZyBkaWdp +dGFsIHNpZ25hdHVyZXMpDQphbmQgcHJpdmFjeSBhbmQgZGF0YSBzZWN1cml0eSAo +dXNpbmcgZW5jcnlwdGlvbikuDQoNClMvTUlNRSBpcyBidWlsdCBvbiB0aGUgUEtD +UyAjNyBzdGFuZGFyZC4gW1BLQ1M3XQ0KDQpTL01JTUUgaXMgaW1wbGVtZW50ZWQg +aW4gTmV0c2NhcGUgTWVzc2VuZ2VyIGFuZCBNaWNyb3NvZnQgT3V0bG9vay4NCqCC +AxAwggMMMIICdaADAgECAgECMA0GCSqGSIb3DQEBBAUAMHsxCzAJBgNVBAYTAlNH +MREwDwYDVQQKEwhNMkNyeXB0bzEUMBIGA1UECxMLTTJDcnlwdG8gQ0ExJDAiBgNV +BAMTG00yQ3J5cHRvIENlcnRpZmljYXRlIE1hc3RlcjEdMBsGCSqGSIb3DQEJARYO +bmdwc0Bwb3N0MS5jb20wHhcNMDAwOTEwMDk1ODIwWhcNMDIwOTEwMDk1ODIwWjBZ +MQswCQYDVQQGEwJTRzERMA8GA1UEChMITTJDcnlwdG8xGDAWBgNVBAMTD00yQ3J5 +cHRvIENsaWVudDEdMBsGCSqGSIb3DQEJARYObmdwc0Bwb3N0MS5jb20wXDANBgkq +hkiG9w0BAQEFAANLADBIAkEAoz3zUF0dmxSU+1fso+eTdmjDY71gWNeXWX28qsBJ +0UFmq4JCtw7Gv4fJ0TZgQHVIrXgKrUvzsquu8eiVjuP/NwIDAQABo4IBBDCCAQAw +CQYDVR0TBAIwADAsBglghkgBhvhCAQ0EHxYdT3BlblNTTCBHZW5lcmF0ZWQgQ2Vy +dGlmaWNhdGUwHQYDVR0OBBYEFMcQhEeJ1x9d+8Rzag9yjCiutYKOMIGlBgNVHSME +gZ0wgZqAFPuHI2nrnDqTFeXFvylRT/7tKDgBoX+kfTB7MQswCQYDVQQGEwJTRzER +MA8GA1UEChMITTJDcnlwdG8xFDASBgNVBAsTC00yQ3J5cHRvIENBMSQwIgYDVQQD +ExtNMkNyeXB0byBDZXJ0aWZpY2F0ZSBNYXN0ZXIxHTAbBgkqhkiG9w0BCQEWDm5n +cHNAcG9zdDEuY29tggEAMA0GCSqGSIb3DQEBBAUAA4GBAKy2cWa2BF6cbBPE4ici +//wOqkLDbsI3YZyUSj7ZPnefghx9EwRJfLB3/sXEf78OHL7yV6IMrvEVEAJCYs+3 +w/lspCMJC0hOomxnt0vjyCCd0JeaEwihQGbOo9V0reXzrUy8yNkwo1w8mMSbIvqh ++D5uTB0jKL/ml1EVLw3NJf68MYIBmjCCAZYCAQEwgYAwezELMAkGA1UEBhMCU0cx +ETAPBgNVBAoTCE0yQ3J5cHRvMRQwEgYDVQQLEwtNMkNyeXB0byBDQTEkMCIGA1UE +AxMbTTJDcnlwdG8gQ2VydGlmaWNhdGUgTWFzdGVyMR0wGwYJKoZIhvcNAQkBFg5u +Z3BzQHBvc3QxLmNvbQIBAjAJBgUrDgMCGgUAoIGxMBgGCSqGSIb3DQEJAzELBgkq +hkiG9w0BBwEwHAYJKoZIhvcNAQkFMQ8XDTAyMTIwODE3NTIyMVowIwYJKoZIhvcN +AQkEMRYEFI/KcwJXhIg0bRzYLfAtDhxRMzghMFIGCSqGSIb3DQEJDzFFMEMwCgYI +KoZIhvcNAwcwDgYIKoZIhvcNAwICAgCAMA0GCCqGSIb3DQMCAgFAMAcGBSsOAwIH +MA0GCCqGSIb3DQMCAgEoMA0GCSqGSIb3DQEBAQUABEBrhZ8JVqGZ+S7dh+xKDwbF +yRXiOQWEa2uxD1O5fD02VfGEzDSrV1sPdQ8AcM3o+ny5AyC11E4Fns2cIkXwZEwz + +------F0DD27AD8CDFF9AB55B87EE3A626E6D7-- + diff --git a/demo/smime/client.p12 b/demo/smime/client.p12 Binary files differnew file mode 100644 index 0000000..b7b985a --- /dev/null +++ b/demo/smime/client.p12 diff --git a/demo/smime/client.pem b/demo/smime/client.pem new file mode 100644 index 0000000..0eeeefb --- /dev/null +++ b/demo/smime/client.pem @@ -0,0 +1,36 @@ +-----BEGIN CERTIFICATE----- +MIIDDDCCAnWgAwIBAgIBAjANBgkqhkiG9w0BAQQFADB7MQswCQYDVQQGEwJTRzER +MA8GA1UEChMITTJDcnlwdG8xFDASBgNVBAsTC00yQ3J5cHRvIENBMSQwIgYDVQQD +ExtNMkNyeXB0byBDZXJ0aWZpY2F0ZSBNYXN0ZXIxHTAbBgkqhkiG9w0BCQEWDm5n +cHNAcG9zdDEuY29tMB4XDTAwMDkxMDA5NTgyMFoXDTAyMDkxMDA5NTgyMFowWTEL +MAkGA1UEBhMCU0cxETAPBgNVBAoTCE0yQ3J5cHRvMRgwFgYDVQQDEw9NMkNyeXB0 +byBDbGllbnQxHTAbBgkqhkiG9w0BCQEWDm5ncHNAcG9zdDEuY29tMFwwDQYJKoZI +hvcNAQEBBQADSwAwSAJBAKM981BdHZsUlPtX7KPnk3Zow2O9YFjXl1l9vKrASdFB +ZquCQrcOxr+HydE2YEB1SK14Cq1L87KrrvHolY7j/zcCAwEAAaOCAQQwggEAMAkG +A1UdEwQCMAAwLAYJYIZIAYb4QgENBB8WHU9wZW5TU0wgR2VuZXJhdGVkIENlcnRp +ZmljYXRlMB0GA1UdDgQWBBTHEIRHidcfXfvEc2oPcoworrWCjjCBpQYDVR0jBIGd +MIGagBT7hyNp65w6kxXlxb8pUU/+7Sg4AaF/pH0wezELMAkGA1UEBhMCU0cxETAP +BgNVBAoTCE0yQ3J5cHRvMRQwEgYDVQQLEwtNMkNyeXB0byBDQTEkMCIGA1UEAxMb +TTJDcnlwdG8gQ2VydGlmaWNhdGUgTWFzdGVyMR0wGwYJKoZIhvcNAQkBFg5uZ3Bz +QHBvc3QxLmNvbYIBADANBgkqhkiG9w0BAQQFAAOBgQCstnFmtgRenGwTxOInIv/8 +DqpCw27CN2GclEo+2T53n4IcfRMESXywd/7FxH+/Dhy+8leiDK7xFRACQmLPt8P5 +bKQjCQtITqJsZ7dL48ggndCXmhMIoUBmzqPVdK3l861MvMjZMKNcPJjEmyL6ofg+ +bkwdIyi/5pdRFS8NzSX+vA== +-----END CERTIFICATE----- +-----BEGIN RSA PRIVATE KEY----- +MIIBOgIBAAJBAKM981BdHZsUlPtX7KPnk3Zow2O9YFjXl1l9vKrASdFBZquCQrcO +xr+HydE2YEB1SK14Cq1L87KrrvHolY7j/zcCAwEAAQJAUkHlWZmSUZMNf5nOpMkM +hZ5E1v2Wjy4UFgRGDcTXbZm401Avwrc8006qQxQjoH94pVkwPYjEyAMubhusMFt4 +AQIhANeB4qzW/YsABYpt66x1ByiuUJE1p+QMLngeESJbc989AiEAweoMtXsNRJi4 +1xBzlKB9zljQfESYIt59SctURPfX/4MCICS9kwyOdplM/qTUCprTNM49saSf9iiN +3xpBXgBygPWtAiEAmoIeDEB26vBxX1OJdKSIeXFE9a9GNYpn8/OiOq3smncCIGIg +Hj9MtdZ/1uPk7jx5oZVfzN1DM7GYZHAeYeYhVR13 +-----END RSA PRIVATE KEY----- +-----BEGIN CERTIFICATE REQUEST----- +MIIBEzCBvgIBADBZMQswCQYDVQQGEwJTRzERMA8GA1UEChMITTJDcnlwdG8xGDAW +BgNVBAMTD00yQ3J5cHRvIENsaWVudDEdMBsGCSqGSIb3DQEJARYObmdwc0Bwb3N0 +MS5jb20wXDANBgkqhkiG9w0BAQEFAANLADBIAkEAoz3zUF0dmxSU+1fso+eTdmjD +Y71gWNeXWX28qsBJ0UFmq4JCtw7Gv4fJ0TZgQHVIrXgKrUvzsquu8eiVjuP/NwID +AQABoAAwDQYJKoZIhvcNAQEEBQADQQAQho0UsOhmuUUhJrx7uXIEbo984OrOVpa6 +giAU7socmyjCzJvihmr/Nnqub+Md7rkSfDytGDN6CianGL5MROjr +-----END CERTIFICATE REQUEST----- diff --git a/demo/smime/client2.pem b/demo/smime/client2.pem new file mode 100644 index 0000000..166919a --- /dev/null +++ b/demo/smime/client2.pem @@ -0,0 +1,36 @@ +-----BEGIN CERTIFICATE----- +MIIDDjCCAnegAwIBAgIBAzANBgkqhkiG9w0BAQQFADB7MQswCQYDVQQGEwJTRzER +MA8GA1UEChMITTJDcnlwdG8xFDASBgNVBAsTC00yQ3J5cHRvIENBMSQwIgYDVQQD +ExtNMkNyeXB0byBDZXJ0aWZpY2F0ZSBNYXN0ZXIxHTAbBgkqhkiG9w0BCQEWDm5n +cHNAcG9zdDEuY29tMB4XDTAwMTEyMDEzMDMwNVoXDTAyMTEyMDEzMDMwNVowWzEL +MAkGA1UEBhMCU0cxETAPBgNVBAoTCE0yQ3J5cHRvMRowGAYDVQQDExFNMkNyeXB0 +byBDbGllbnQgMjEdMBsGCSqGSIb3DQEJARYObmdwc0Bwb3N0MS5jb20wXDANBgkq +hkiG9w0BAQEFAANLADBIAkEAn9qneRYTKPokme3obiJa2NTz1Z2kcF3NHVh60Qod +/TV/q4olPrZdFR2TDWt63Lgnygcsgf3u9pnhcEGk6IvntwIDAQABo4IBBDCCAQAw +CQYDVR0TBAIwADAsBglghkgBhvhCAQ0EHxYdT3BlblNTTCBHZW5lcmF0ZWQgQ2Vy +dGlmaWNhdGUwHQYDVR0OBBYEFDKWFe6VWMhtRTE3/78+hAnSGxmvMIGlBgNVHSME +gZ0wgZqAFPuHI2nrnDqTFeXFvylRT/7tKDgBoX+kfTB7MQswCQYDVQQGEwJTRzER +MA8GA1UEChMITTJDcnlwdG8xFDASBgNVBAsTC00yQ3J5cHRvIENBMSQwIgYDVQQD +ExtNMkNyeXB0byBDZXJ0aWZpY2F0ZSBNYXN0ZXIxHTAbBgkqhkiG9w0BCQEWDm5n +cHNAcG9zdDEuY29tggEAMA0GCSqGSIb3DQEBBAUAA4GBABpE9xt1Hlq2dQZUXHuX +HI57vc2mlWnhhM0wnNhsNZFwfXRHCZOo/JJBhEIT3Rgyz0ErrbOr1SN96HNDKXOD +z6bh4NxB5DZ9sRPKEBj66zDsWJVMlom+Lkeal+GkVy36vpAyP1r+cTXyc9M2Gw/o +FBMinMHH/BXvF5GJ+UleheZe +-----END CERTIFICATE----- +-----BEGIN RSA PRIVATE KEY----- +MIIBOgIBAAJBAJ/ap3kWEyj6JJnt6G4iWtjU89WdpHBdzR1YetEKHf01f6uKJT62 +XRUdkw1rety4J8oHLIH97vaZ4XBBpOiL57cCAwEAAQJANXfspprUo9MvpPEn2pbR +Lk/kk2IcW510e0laI0uwBj50djfHqvsU5ccuVLrxowngLGrFmM3G4lnMknR2NvH8 +0QIhAMsK0AwStUNM/KyvIMikHHBOE9PrK7ARgKvlKl+0ieWPAiEAyYwonIVAtr1f +M8vmrc6TM2YxzSq4+jyYktaaNhYw11kCIA5pmhMBUPSSBm2LkNwtKgeewzGLw/If +i+6nubZJbnBpAiEAvJQvy4PCsTkvQr+d7zJB+O2920IGId1gxMOXNtQ8jsECIGvn +Uz54oonshmTg+Kj2DxnUKQEzFAmQLbtFslp1m47v +-----END RSA PRIVATE KEY----- +-----BEGIN CERTIFICATE REQUEST----- +MIIBFTCBwAIBADBbMQswCQYDVQQGEwJTRzERMA8GA1UEChMITTJDcnlwdG8xGjAY +BgNVBAMTEU0yQ3J5cHRvIENsaWVudCAyMR0wGwYJKoZIhvcNAQkBFg5uZ3BzQHBv +c3QxLmNvbTBcMA0GCSqGSIb3DQEBAQUAA0sAMEgCQQCf2qd5FhMo+iSZ7ehuIlrY +1PPVnaRwXc0dWHrRCh39NX+riiU+tl0VHZMNa3rcuCfKByyB/e72meFwQaToi+e3 +AgMBAAGgADANBgkqhkiG9w0BAQQFAANBAHI5KXfL6kIRoNjR8G9/uKiPUt4uVBKF +ecGp87M5t2a92Z0KpWOMXSHZ0LLQKqwWzALvWcPPIj6S8F6ENdwpfMk= +-----END CERTIFICATE REQUEST----- diff --git a/demo/smime/m2.se.p7 b/demo/smime/m2.se.p7 new file mode 100644 index 0000000..3a5bb1b --- /dev/null +++ b/demo/smime/m2.se.p7 @@ -0,0 +1,84 @@ +MIME-Version: 1.0 +Content-Disposition: attachment; filename="smime.p7m" +Content-Type: application/x-pkcs7-mime; name="smime.p7m" +Content-Transfer-Encoding: base64 + +MIAGCSqGSIb3DQEHA6CAMIIOBwIBADGCAVkwggFVAgEAMIG9MIG3MQswCQYDVQQG +EwJTRzEvMC0GA1UEChMmRmFzdCBTZWN1cmUgQ2hlYXAgQ2VydGlmaWNhdGVzIFB0 +ZSBMdGQxMjAwBgNVBAsTKUZhc3QgU2VjdXJlIENoZWFwIENlcnRpZmljYXRpb24g +QXV0aG9yaXR5MSQwIgYDVQQDExtGYXN0IFNlY3VyZSBDaGVhcCBLZXlNYXN0ZXIx +HTAbBgkqhkiG9w0BCQEWDm5ncHNAcG9zdDEuY29tAgEDMA0GCSqGSIb3DQEBAQUA +BIGAaH1UOF0vZjCfEDbzWAy+kni/T0AVVbTdWeCZAaADIgNrUqVJKkV8BIrAFRUO +jNdz23gra1TRgoe+sq4qn9WBi61+f+8JUV4UHZYl4DdcIkcGvdEI7utPLFNFif2m +oqd+kr39wuGWMonoBpzFdycSN/FRpFTrwoyXEIHj9VPCwvUwggyjBgkqhkiG9w0B +BwEwGgYIKoZIhvcNAwIwDgICAKAECCZMYhSx21tlgIIMeGyY4Sgm4uv0a0Zu5gnx +x1bA6wPY2W8oOqFC3jEAc6j0aP9TzxFW33W5fT/wCWCuIaS8Bm2nCozt7yhBofV2 +9mEtlahbEV1KeqwgI/RteTHD2WUvz6277O9lsB/xsreKM63WzxkpTWi0BUQBDjC1 +ncwy/74ujmZhdAD28HJie5cqhxKQd8if5/Ild3B786sxpGaSBKbJAtJYG30imbHL +B7JTqDciCKQZzFeQBc1TOcld9VqFP2dRexmcPMxcyJN1iJe8R0g6PYpfAmLfmN6B +fAImcgJYBpq8PYzmTi7cpJCMmzlPn7urm8Nx4DUQtzzMArmSuMfWGhu2wWLM+2Mn +TB1cAVuO0bGgsHnr0BKW5gfQEJEIZY41Kq179RKAzI6OdJYvnKXoadEtzYI07Tup +D+HJXLdAuU/B6E+09IS8mg2RydNyRP+lp+sOVve6L4LAH8h5pt7hoES5/qgiew09 +FPpx5xmhcGCTHJ9Ad2Xec6zAuYkVWAUfLpiy2A2NDTZnQ6EWaeWHtoAizMjHlqQY +ZJYeXINzE4/u9ph5DTvXV2Sd57eOoaaaHmWwoV/887cdW33cQQ8LXciuzduI/b4V +LWFm3X9eVdGEeQFoNDP5D7xwEK67flhufYClB99lt3tCfRLvvcclsd39heOex5/M +6XeA8TeEbuukMhEe6bW2o+E736XNpziFm7hL1UGBXqbv26sN3A30H9LRMXGlZZxj +raGBl8uxzUPi/SxERMbRrYC72x3Dev5m5I458wXEyc94GIqDqcHL0Q+za+3iyC2g +496OsVeqtob9oshXr3rI0FjV+ejNMBcnhWeaPKLl6MOnqI76kczARZR8WrO+Dyy9 +FpgwX/DNKstuw0rLzzN7Zb+J4NPIhlHDt2Hh3KLjoFAiXPSDugKGQiceV6mg0IuS +8HPq8rTiKFEnFtPTWxvAKIOa3dKaOiBoCue1Qm+ffV/2AB4Q4Xf0SpzgDBa6ruRF +MImgcdeNjzu5MggGGHQtYU1uMMqhX4o2GIafVmAO5zUVYHsbQ2Lht+/x+m8nBamV +H8AUzJQBDL10bbmZW8fh4JqwxXsdmoTKSSSKEk1+mP55DeBnP+GPA1EGbZWoI+mO +FoHd1QKDedI/r8o9y1SkfhPxrKl0XKG+VLIUmmP+qev/7s3d5PEfg+ShkAAf2ccP +2o+poSQKHYeOVM/RxLjS7AR96MtMl+46Cyinbl1quRjYLh84iQSP9RNVhVM06fi5 +63RHv8BAlJRnEcyiFNV/uDBYTFQ/2Gf8V9gBjYWyajnISsTsD2ZQ93jT/sjL6KpH +Ouv7LQplVrxkZHXQgOoqd9fI1QR8qyBjoMUivv0DSKPKZo0MT/KfqUxG+DsFgOi/ +a7WW9J+XA7N2hGiP3CEqmm1pPi6IUtQj5kGmjlcEtco9syTwPSh5w+tcmVtQeu1+ +wwg53wSLHUdRUmVGqyCVV55ItkirSQt37JCDkXoivCfbdi5BI7cQ6IBhRvpBw2uV +IA0O9/AfhXhUred974WInrjjlo0RQ4rGZutwDYWrPzWjzuPPvomih6rKQ0HH1bgb +vdAZ+UjXtfSdory36LMg9Yb/oFejm/77NRXAo9AAf2MQvHVN00zKzDoMm4CRiJUc +ZCC2+3ocFEs9l6jOO2tPrQpH+E69XXyqQcer18FyM3rTv81O2SbOz0IfL9vBFEsL +ZZVifPKLt9LfRRjoqehXTxkmfQ2NmHUyksHO4aE2/OrtMVXBv35WFzLPIsX9C+yX +r6+QPQrkG2Uad2e0YM0ipNPk4OIy5Bb+Ab6fIbHvLgLBRv0yBZ+e9OoEZ1UFC6sR +EncKSV1fkRlemGgG2cEP6Ceq+x0zbS4A97nqQO5Hks5Hv5PqVuSaWSIjAEFihmpt +wB41IunnO7b/jzJiNLK28e83uICuYgrPPEeYbbdGrEn65ZzdvhOQduwj0baJRp3V +0z4boIs7oODYq0IXklvoMzwoaAPbMo2IBamMbZbY7HQbPOeI9QQTkBmKjavFRg/X +fismhuFuVbyMQPc9uV27xpI2nRSkfN1X2L1lz+e5MMEwHqVRER3tGB1/g+WMrSGV +ORvXam6fmkIgC4bCSAGBPtqHr2G2rzphsxESw2pbk/7ll8lGuYf3ZxovXJiLb2RB +9j9m5TFoZ72Asplc04NDKykVOzZGWvNJCCV9zUHTyZ6X4qx+C4P6g58OvyZEf2VB +arkD40QoEBkhNP6QLfacdFNAegWp6MwJHCDg2YIeLMfgCKkDVP+/SijiT3x7ahHO +n3ZxKsq1bC6N7McwHB6gzkdpSSqWdtSv+fh5TNlMf7oCH0Laa/2k8S7NDpNbIeqr +X46ECkmqPTjt5DOuEp3T5q4sOXV2Y3m7FegD2J2WrpXy+/GNgSVB2Kb7xzhR13ve +Ipu5V+FFa6Q/jbXxp864ZFE6WuZYBYjrkBns2MaF+9RpUSIzyTYxJmB9pY0SwrzX +2WWnxCCt7cwB1JL0jbrCz0vZvZwDRbtCyepBYel0xqKOhp1dFPCw2p45ZbQJwhKx +nFMDUzlDZjiAoUdq5oV8TmqcjbGDmLndYawHPqD8m3g+glsCaFI89Dny8NsT0zv2 +vHCt6IbbpVY6j/Pwg9SFzWwnzGr2s5aL5MEbpzEyfiuYAe1hPdue2gWQO1rvdIBB +zJwdqbrYb3qzJqrqrPQewSpsIzEWJmc5NjATIJOEtgM+Q4+FFTf4f1rZ2HAsK7MG +aIMZFyjoyvqBHG/IwCh+GBLLIw3IPRJtDcKf/nDZqUCw0wHpTcXu1nHJllNmtNcI +Xw03k7WctqlxTUBFUjiC6XcU/yAlLZ8UeyLF3+/tSyyV+Ve17cZfAH63WH+oaWL8 +yiC+UH5tpZQWCSBvCz81ezHOBefefOxeIvq5nZyEyP23ltaV/DJqj1a/5pn2VH63 +IrRlQF04ATaUAowxlNIaqD5Sk6tzPKp6nxOO3vP8xRGT1SFKMaNCuJRaZXXQvYcg +zyiQ0vtli0g8cdbR8dSSTAP7b6CAqb2dYHwtJVUzPzWbWDIpRzrXOdoI/lze+fwA +xYwAFoTuLLrKgbkOB8HE02ZN4exCp2dcMfwt+GCgYgCtE1/xCHd4RMg3yHxBBDQQ +uk6dL6dXSD8cw2TqzrJRU8Xk/pLHeQCf694W2flROD/RF3avgcjnJ+0E+CJXmU9c ++uAhFRmdgRzM6G+h2p/MF8HjZ4MZTR6vA4v6mvTLLBbwIUfVNtXIwXpFqQPXVRvA +4Q8HmLIX2fXGzR2Nw9UU0GBitT4r1isMHCQwVK5kqw3XyH15oIXCCsE1/ftBun8n +SCQ2OkLHuZ4JWMIYVpTtDn3jd0Gyu6x8hBK+HpX62wnKZZA8mxzMoInAFK08s0Ie +G3m+bjX7WsdE1eM0TSBvYv2d8CNqaqxJkF/ZlxkAKV7NoP0isuRJSSHH0IcrWNR7 +9mtfxV+ccBUqqO7BmlzLYPjnN+7hOJbYsm0ZJnDZIn6Oc6t8ti+oF4QmUBQ2u7As +8uBJLbd7TghA7afcrurQAY8gJTq4Q7/lpYZSZK+W0I4fY04YhAfjFuln8r6w9gqr ++Iwg58dbq7MomCsg0F687kApYygyYQeCsio64tb3sXkCmDX2rdRoAELLf3AYccn+ +BW9iru372oWru/kTQTj6Uziv4TiyjUGQEYhtjweTNFTXmYrxYHgb2Hzu6TNIG9Ev +4ORq9KL1uWSK8zJ6eL9CZ0kSr/4+SBHFbGupfGXz+fZysu3nFJ1oO86vMOFG/UU9 +24+jbRWklu0fyXZfeNzdY6arTLOX5Gpc9PRNZILoFHGyB2M9ZmMYz3hSNzY8acpk +XDi+2ycHYeWFGxGCLio9Lc2H6IOMqJTTCpUduWvBX4tfysanUNjOuKJ3FWKWLxda +IX1jJbPjvrQ3bg/NU/MqliLwFiMA1WkJd/paeK//J2tcOIMyhc40sigu4z65VoJ5 +AsMHlvh1dcfjz645NOx6JOpwdd04hoOxlMDtRTE/b8yjwaV2EVqXnI5WkNwCQESU +ayIJiQqDfn35EFY8Wkmt7f6YGkpqoBDJkyNadsrcd2YVz9paAhgcM3XjC7zPi8AH +g9UiTQiowRLcvnxkHK0LxelsQTE+ENL0rwYCt+qHBBJlcLVZhDF+qAchv+G5hmxp +CClPyvbzOsx7ykcvItE2fpBWJu27l48dFxFviOKY7ecvJlpcTAX83N79k0aJpOC+ +TEPMdMZjIeGn004FiVj9Mo1899e2IlOlNdmj8Mv/jW4sFKpJ8m5fHv8i/ax7W33D +0GNMrEghOH1w+QAAAAA= + + + diff --git a/demo/smime/ns.p7 b/demo/smime/ns.p7 new file mode 100644 index 0000000..d2ab1ac --- /dev/null +++ b/demo/smime/ns.p7 @@ -0,0 +1,53 @@ +From: Ng Pheng Siong <ngps@post1.com> +X-Mailer: Mozilla 4.5 [en] (WinNT; I) +X-Accept-Language: en +MIME-Version: 1.0 +To: ngps@post1.com +Subject: S/MIME signing +Content-Type: multipart/signed; protocol="application/x-pkcs7-signature"; micalg=sha1; boundary="------------msD996346BF03A805B450DC20D" + +This is a cryptographically signed message in MIME format. + +--------------msD996346BF03A805B450DC20D +Content-Type: text/plain; charset=us-ascii +Content-Transfer-Encoding: 7bit + +smime -vs- pgp +war over? +--------------msD996346BF03A805B450DC20D +Content-Type: application/x-pkcs7-signature; name="smime.p7s" +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; filename="smime.p7s" +Content-Description: S/MIME Cryptographic Signature + +MIIF8gYJKoZIhvcNAQcCoIIF4zCCBd8CAQExCzAJBgUrDgMCGgUAMAsGCSqGSIb3DQEHAaCC +A9cwggPTMIIDPKADAgECAgEDMA0GCSqGSIb3DQEBBAUAMIG3MQswCQYDVQQGEwJTRzEvMC0G +A1UEChMmRmFzdCBTZWN1cmUgQ2hlYXAgQ2VydGlmaWNhdGVzIFB0ZSBMdGQxMjAwBgNVBAsT +KUZhc3QgU2VjdXJlIENoZWFwIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MSQwIgYDVQQDExtG +YXN0IFNlY3VyZSBDaGVhcCBLZXlNYXN0ZXIxHTAbBgkqhkiG9w0BCQEWDm5ncHNAcG9zdDEu +Y29tMB4XDTk5MDkxNzA1NTAzOFoXDTAwMDkxNjA1NTAzOFowYDELMAkGA1UEBhMCU0cxGDAW +BgNVBAoTD00yQ3J5cHRvIENsaWVudDEYMBYGA1UEAxMPTTJDcnlwdG8gQ2xpZW50MR0wGwYJ +KoZIhvcNAQkBFg5uZ3BzQHBvc3QxLmNvbTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA +vp8eCa+KpzNCv0MWyicUImP+WHxlrxm5EummI1Qe77U4w0k8IQuue7QIURWKDjdZI97izzeQ +SozHvgSsjCvuJlqifMoV0v7U4iUQoZkrXO3hzwM5VNr875M95SYeBjqWDUc0v3R6tka3xhg3 +dMoEL3QR6gsiardPEwygdL7/FN0CAwEAAaOCAUMwggE/MAkGA1UdEwQCMAAwLAYJYIZIAYb4 +QgENBB8WHU9wZW5TU0wgR2VuZXJhdGVkIENlcnRpZmljYXRlMB0GA1UdDgQWBBTEHU/WosFL +yRo8NT9ViQ8APbKBXTCB5AYDVR0jBIHcMIHZgBQOTokwY/jsgXmHJoyQ5DmTVN0c8KGBvaSB +ujCBtzELMAkGA1UEBhMCU0cxLzAtBgNVBAoTJkZhc3QgU2VjdXJlIENoZWFwIENlcnRpZmlj +YXRlcyBQdGUgTHRkMTIwMAYDVQQLEylGYXN0IFNlY3VyZSBDaGVhcCBDZXJ0aWZpY2F0aW9u +IEF1dGhvcml0eTEkMCIGA1UEAxMbRmFzdCBTZWN1cmUgQ2hlYXAgS2V5TWFzdGVyMR0wGwYJ +KoZIhvcNAQkBFg5uZ3BzQHBvc3QxLmNvbYIBADANBgkqhkiG9w0BAQQFAAOBgQBN4w1l4IXm +DJke8XVyo3SFlre1nL/bjDB+hpYCgTQnceHAmSi6L8FljGs2KYGFePWLSCsOxA99q8lVtl7a +sdoOtYkTluccWGqnWloyusOzlO2xgpoQxZ2iue3PT4Y4u8czMTs2AyanZyd6NdQoaYGfI2g0 +tpgiDhSqcX8r/JYYVzGCAeMwggHfAgEBMIG9MIG3MQswCQYDVQQGEwJTRzEvMC0GA1UEChMm +RmFzdCBTZWN1cmUgQ2hlYXAgQ2VydGlmaWNhdGVzIFB0ZSBMdGQxMjAwBgNVBAsTKUZhc3Qg +U2VjdXJlIENoZWFwIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MSQwIgYDVQQDExtGYXN0IFNl +Y3VyZSBDaGVhcCBLZXlNYXN0ZXIxHTAbBgkqhkiG9w0BCQEWDm5ncHNAcG9zdDEuY29tAgED +MAkGBSsOAwIaBQCgfTAYBgkqhkiG9w0BCQMxCwYJKoZIhvcNAQcBMBwGCSqGSIb3DQEJBTEP +Fw0wMDAzMzEwNTExNTJaMB4GCSqGSIb3DQEJDzERMA8wDQYIKoZIhvcNAwICASgwIwYJKoZI +hvcNAQkEMRYEFDIu42hsp6w5K/x7ejaVgiqgPYk4MA0GCSqGSIb3DQEBAQUABIGAkipjxn6+ +NhIyRPGgrqY9zpDC8AGdt7sGFMKfxhJ+UoSb+lBH0imHSBhA/BnlfysyyDXCpit1Gxy/W/jn +vHpI0lMLvvcKOtSQp9HUPAtawdeF6Zau8SovSBroUnxxY7DILJoKnaHheHF7G2MbYusyZCmi +r3xZ4P0Ps3fhNEAmrH0= +--------------msD996346BF03A805B450DC20D-- + diff --git a/demo/smime/ns.se.p7 b/demo/smime/ns.se.p7 new file mode 100644 index 0000000..a7a75f5 --- /dev/null +++ b/demo/smime/ns.se.p7 @@ -0,0 +1,75 @@ +MIME-Version: 1.0 +Content-Type: application/x-pkcs7-mime; name="smime.p7m" +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; filename="smime.p7m" +Content-Description: S/MIME Encrypted Message + +MIAGCSqGSIb3DQEHA6CAMIACAQAxggKyMIIBVQIBADCBvTCBtzELMAkGA1UEBhMCU0cxLzAt +BgNVBAoTJkZhc3QgU2VjdXJlIENoZWFwIENlcnRpZmljYXRlcyBQdGUgTHRkMTIwMAYDVQQL +EylGYXN0IFNlY3VyZSBDaGVhcCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEkMCIGA1UEAxMb +RmFzdCBTZWN1cmUgQ2hlYXAgS2V5TWFzdGVyMR0wGwYJKoZIhvcNAQkBFg5uZ3BzQHBvc3Qx +LmNvbQIBAzANBgkqhkiG9w0BAQEFAASBgERIsZhX2nmvB4kOtcfqUpuWjK9njs8YixtPSS4w +ugmAx3LhBMTgY1OlsbKi7etVPwEU2jSqgkwCHPeYeOQt6M9aQ66tcue7/fWjfMBfZ+tetwOb +kpqlw//GyjhkJx2QBGPuw3Ye84ll1KA7x/CGxmk8g0YBHoLo9Xy1W1XHVUanMIIBVQIBADCB +vTCBtzELMAkGA1UEBhMCU0cxLzAtBgNVBAoTJkZhc3QgU2VjdXJlIENoZWFwIENlcnRpZmlj +YXRlcyBQdGUgTHRkMTIwMAYDVQQLEylGYXN0IFNlY3VyZSBDaGVhcCBDZXJ0aWZpY2F0aW9u +IEF1dGhvcml0eTEkMCIGA1UEAxMbRmFzdCBTZWN1cmUgQ2hlYXAgS2V5TWFzdGVyMR0wGwYJ +KoZIhvcNAQkBFg5uZ3BzQHBvc3QxLmNvbQIBBzANBgkqhkiG9w0BAQEFAASBgKFEdyFcaj2z +7LBp/rdEiDRNkyvjZtpEc3F1tvml2jmOuRZq/Gd0Ib4ELmAuC2qZ7AG+MNwlMeCQp1PKGjCk +3Af9hjcOX27c+qF3pSK35WOPrdQTaVLjqdNwejInEoBjGlHa83V1L/MTiD5ABOXGIZKOFS8m +G0tPxoc3AhlYbHspMIAGCSqGSIb3DQEHATARBgUrDgMCBwQIvFFOv2EOARiggASBsBBa97DM +xPCX8mZ2K/jjVl2LKtBP0uwG+nd48IiaoBERbJohEdzxTlIU3pj+QZ4ktmSSYo/4Os0eSCfK +v2mG3d5SPxcE6HztA4FttvQ4IiByTQUU61vJhMoirV5Nvfkxk8i8xPMKMlJSruFBnIOteXQv +vwVwBCIAkMhxBE9Sj+OfW5966d+LmB3yGEZa9GEW//4VbR6DNs1frEz7riUod0om5CA66bou +8kXxGTZ5fYydBDDdrsgcU7DBTlw2XayUmYS3Gp0vQvW4l/iiYEeYh+OrKkGYQGdsb5CcZr0F +FfT2PRAEICA58XUUrt//zi0TzBTwDKAWoJWe/QfOXXEVQsfqCl1iBBhACmC4JLQRSulBCCpA +F3E3daztLz81ycQEIDyHXxMGZFzZQCvy51fN7eRLl+yACoXKxKtVs6EKpJq9BBDDGI8CWqTZ +DIMC98l4AV76BIIBAFKj+L6ZG8Sew6IpE5UVq1oS42UrmUDQRrYpZX3OHC8I9pr7mtlU/V0H +mA55PmixsjcchT6wOF2wVD9G0rBCOlUfuYNTkhR/k51floP9fk9Oy69+jXxUb7r0AF1M2b13 +XQpmnz6YPQPdPxgi8i34iT1oqRY1d4Nntmgl/3xbQ/9MGbgZQSDbduRiy+Zk3JPZzMSVWEVP +Pt7anOrPmCKDAvlzpX9yKdyE4Ug68s2///S1JjCjGd65007oPXME3S+7WWylQXdjCe8EM/Li +IE33DcRUFvbuo7q1ftxbwRKn32+Z1gkiG4j+DQ4fTO0RrhnTLUpaaOaX4N3psjSA/3Y+vKYE +CAkYtbSSwRr5BAhmVVsaf0vm7QQIE77+DXI3Gt0ECIs/MQuPnwd6BAjpq83cshzQ1QQI3Soo +r1XwQYIECES6t6POe3yxBAgmObWcpGTawQQI+6e28OKubqkESBlD4aGRdfOXkGSw5zN1pVK0 +s37qLWOsO+jClvd4EmfB/JI/ABieNmfBHx2jweIazMA2b9wylRvYp/TMiDFkRl/thDqyj/sC +7QRIypPg5i7a745PX+HcyHhcy5lUxkpnK5pjAAHSUPTXNc/3H6PpU/8+qiPncfqNXVdoiiSa +VMSmFMZI+YGDyf4twVuGZbdR5JV2BFCbQNJeSkEhEnZiMKshmNWXbjs4DzYlTO0Pq5D+9RYJ +1iy/kahcms+2nDNlrVf6jH/lCTpPCM/OJWrjaTdpiFzp/eEY39qbWk5AcIFYQ7xIcARIHVPX +uhyuO+5BmO0MdMMOL/KlhRtosKHHRG7PH9huyWYFloA4aeoiOxeXkDJk3y0NkG+nJjdM/gWT +1J2HJi8opt0F0bn8t7MsBEgt2VgTZmkHT/yaJDInQEqrpe9PJ412M+2/kXAwt4VfYrEC3ocQ +WQuNEpFc1kb6ZGJSYz0TvnW8DbP/GoZqQpF17p1vs1T0QhIESC1qr8SbwXjHjOdAh94Lp5jG +b8kAl20P0hqqL+5qq661JzAdGotFN7sMOe07Xn3eaYEVe4htVHlIfYMPH2B0CYyHu9LH2ueG +gwRQ7Dv1hygub3ioK/s9CkgAi83qUENz5RWYMtIiP7tbv34zq6DKwv0ioBY4MEKfR4cCuGzS +6I67vTvCgXPVxW9qHtoTTDfufUR6yOOa/gvuZt4ESN3fo5Kgpm+74AMeOVR0RZ1lZ+75E9KV +wmiBGm5ORHGSmyt3MQMW+5UEloz8mxK39O6RtKBL7iKH3d0clJn63KXvUsf2l7/8swRIIVzc +1jGzpwOsUseKHaeLQuknjTuXAlyCTQYa3C0gEcEBElJY/TMevV+ZrlBB/nCwyaYbmH+WxxfQ ++xF79+eIZ1+L5zGAlSNUBEhO1Gy1T77JUa+s4UUqGP+BNNsGDuR4YXWfCIA2vBfCFZVggkaD +v1+uX2Rgim39wzMCLkXQ8ZXLWdWo7/XcllOrxJSH84uPat8EULZTtkbY1oW90JzGEJKbOkEA +YaoQ7I0dKXnkLsY3Y54p1WW7fPQ4JLLhld2LpmZbYOntRJmn8LZWGWv9J3uYlAk9ujAfF10f +7/qV2zDB//oKBEih8QUPicP+mSdpvRlws5RfiKmcf9W7btoNALqqA2COEOHjjJHdX9b3EVlk +vftz5n2AbaS7fenhIl1/rbhmBfub3eHwmInRoUEESHcpUKHCEe3dtGe6vB2VXR4U/MvKScZV +RpHv6KGj0pFZwNasoZ9W5RB0dwAbhuD+xiimlv/m9XwhIQihgTUmarVdEbYcbl1aHgRIFMEp +bOJ4D49qYR9DXZprlST9nZTgU/R1KTcZe2LsXBNa9xxlO9NYvZCSQSuRndIIwbAXjgezK1d0 +fk+JSIyC8kaQzWBwDGdXBFDA3tUZzKed3JM+R/a8319iAemyyYgvk/izKKXncpOfCdBtVgHc +cOC7LPs1BGxqGCyOzgD2KMQSclRZJo+siuRvJ2wlKCPjd3DHMk0IJ6EYWgRI3XuDbhKfLzEQ +h9G5n3wQr8qizabBZ+xZEmJk7AUxA7ykEdSuLlV7qP/ujj9sZ9e/AaHG+mfjGRI01OPquZxW +Pwt9nRpg1KqCBEjFQqB+hbkfT218E5+mFX/aotUq+tEoZ1Tg3Lq5WENZVwMNR0eFR69vN+gL +SZU3nnLPEchS3pasz7ZBohL+kEBad4fZfKNWUa4ESO9adCvusxTvjlvf8Gu/lXU17gZuAHsj +bMwSWyQeZKObCp7kOtK1nQCtLHkWHEpmx16eQWG57+makKYpjD4BSXFXGEwMf3Pw9AQQd6Po +6JLMr7k5LjkOcyCe/gQIJ2ORlLJDWKMECHz5jFYLkHrLBAhPDugA9fP+MgQIQq/w/QD4YBIE +CGeYC0G4o7ElBAj3O1FpJvdU+wQIiOZHO6By7rAECN9TZKOJa8YbBDD/7QgWqJSNhck7fNEY +tHmopW9OWfXfXUInzaQFBtLauNRj0C2OW/zB5dgsC3+9/jYECG3bevzH5HiDBBBHiuTo8yYJ +1FFJ/y4nIIXaBDC92swMrVUYZUpoNUjf02DsBiVP53Fon+CnKnEQ8gBCncIkb8DBnlZYeZd2 +XISBWmEECNOMqXZKxL2vBBDDg2nHASKLEobcHqmPoIQFBBhcrb6prkWNKColNlf5ss49ocnK +68UgsNEECNJJEPZOUMW1BBApQ6CzF2t+QiJTdQwoApPzBBg8ZCeSHgypK9CWcWb2iMWiFuKc +KUmIuskECFr2TQ/U6cWbBAisYHDp2/w4kgQISMieG/GdQlAEEGLN6GdGVz2wf1Hx93PJ4VsE +EJRdLeaU3FsCwXEICvSKZPMEEIcEdqQkAhmsxfNoKA90QlUECDdUY0tvC48sBBBNOevphvxS +0dwE2Lzf6gLGBAjoSuhhSYuZBAQIRKWk1TxshoAECFUuzdHGuR3zBBjlvJQjSDrEPqg7unh2 +ZaNy11cUKB07vPMECIbZbuiAcb81BBCyb6N/Aymqo24VwyE9/lTDBCCR0Sn7D8OfmpwUp0MJ +zEdp+4eTPVu9W6bNj5cTwcG0VQQI9Z1/nYjZ93cECCZ7u+8p0rP8BAg6S5MaaL8mTQQIMt+W +TF2XgOMEQLOZQ0FOc4oD9mBoXzN/NDmlEz0QWnZPnO/7OhVcuKPi6rtE+5RfdGDL/qxWGU6G +0qvFT0zTodnwjpzKnYAnoeMEUDPxz4djUsMXhHzpye9vc7N2/B8QUBubRaFPp/FoLv3jjwga ++AawOtkkW1uLku0m6zdN7YYzXTsKlMhwDd3nZS4Vm/v11AnFChiDC2LFmLw0BCC/X36boypP +pTObDByKlN6IldnnPEi4tl/6Cxr2+28LWgQohCdg9nl3s5COEnwQW+ggcvKodHHK9SdVRscS +J4ykS0lWZ0ns4/g6/wQIKIXduWxdKJMAAAAAAAAAAAAA + diff --git a/demo/smime/opaque.p7 b/demo/smime/opaque.p7 new file mode 100644 index 0000000..a242446 --- /dev/null +++ b/demo/smime/opaque.p7 @@ -0,0 +1,47 @@ +To: ngps@post1.com +From: ngps@mpost1.com +Subject: testing +MIME-Version: 1.0 +Content-Disposition: attachment; filename="smime.p7m" +Content-Type: application/x-pkcs7-mime; name="smime.p7m" +Content-Transfer-Encoding: base64 + +MIIHHAYJKoZIhvcNAQcCoIIHDTCCBwkCAQExCzAJBgUrDgMCGgUAMIICQwYJKoZI +hvcNAQcBoIICNASCAjANClMvTUlNRSAtIFNlY3VyZSBNdWx0aXB1cnBvc2UgSW50 +ZXJuZXQgTWFpbCBFeHRlbnNpb25zIFtSRkMgMjMxMSwgUkZDIDIzMTJdIC0gDQpw +cm92aWRlcyBhIGNvbnNpc3RlbnQgd2F5IHRvIHNlbmQgYW5kIHJlY2VpdmUgc2Vj +dXJlIE1JTUUgZGF0YS4gQmFzZWQgb24gdGhlDQpwb3B1bGFyIEludGVybmV0IE1J +TUUgc3RhbmRhcmQsIFMvTUlNRSBwcm92aWRlcyB0aGUgZm9sbG93aW5nIGNyeXB0 +b2dyYXBoaWMNCnNlY3VyaXR5IHNlcnZpY2VzIGZvciBlbGVjdHJvbmljIG1lc3Nh +Z2luZyBhcHBsaWNhdGlvbnMgLSBhdXRoZW50aWNhdGlvbiwNCm1lc3NhZ2UgaW50 +ZWdyaXR5IGFuZCBub24tcmVwdWRpYXRpb24gb2Ygb3JpZ2luICh1c2luZyBkaWdp +dGFsIHNpZ25hdHVyZXMpDQphbmQgcHJpdmFjeSBhbmQgZGF0YSBzZWN1cml0eSAo +dXNpbmcgZW5jcnlwdGlvbikuDQoNClMvTUlNRSBpcyBidWlsdCBvbiB0aGUgUEtD +UyAjNyBzdGFuZGFyZC4gW1BLQ1M3XQ0KDQpTL01JTUUgaXMgaW1wbGVtZW50ZWQg +aW4gTmV0c2NhcGUgTWVzc2VuZ2VyIGFuZCBNaWNyb3NvZnQgT3V0bG9vay4NCqCC +AxAwggMMMIICdaADAgECAgECMA0GCSqGSIb3DQEBBAUAMHsxCzAJBgNVBAYTAlNH +MREwDwYDVQQKEwhNMkNyeXB0bzEUMBIGA1UECxMLTTJDcnlwdG8gQ0ExJDAiBgNV +BAMTG00yQ3J5cHRvIENlcnRpZmljYXRlIE1hc3RlcjEdMBsGCSqGSIb3DQEJARYO +bmdwc0Bwb3N0MS5jb20wHhcNMDAwOTEwMDk1ODIwWhcNMDIwOTEwMDk1ODIwWjBZ +MQswCQYDVQQGEwJTRzERMA8GA1UEChMITTJDcnlwdG8xGDAWBgNVBAMTD00yQ3J5 +cHRvIENsaWVudDEdMBsGCSqGSIb3DQEJARYObmdwc0Bwb3N0MS5jb20wXDANBgkq +hkiG9w0BAQEFAANLADBIAkEAoz3zUF0dmxSU+1fso+eTdmjDY71gWNeXWX28qsBJ +0UFmq4JCtw7Gv4fJ0TZgQHVIrXgKrUvzsquu8eiVjuP/NwIDAQABo4IBBDCCAQAw +CQYDVR0TBAIwADAsBglghkgBhvhCAQ0EHxYdT3BlblNTTCBHZW5lcmF0ZWQgQ2Vy +dGlmaWNhdGUwHQYDVR0OBBYEFMcQhEeJ1x9d+8Rzag9yjCiutYKOMIGlBgNVHSME +gZ0wgZqAFPuHI2nrnDqTFeXFvylRT/7tKDgBoX+kfTB7MQswCQYDVQQGEwJTRzER +MA8GA1UEChMITTJDcnlwdG8xFDASBgNVBAsTC00yQ3J5cHRvIENBMSQwIgYDVQQD +ExtNMkNyeXB0byBDZXJ0aWZpY2F0ZSBNYXN0ZXIxHTAbBgkqhkiG9w0BCQEWDm5n +cHNAcG9zdDEuY29tggEAMA0GCSqGSIb3DQEBBAUAA4GBAKy2cWa2BF6cbBPE4ici +//wOqkLDbsI3YZyUSj7ZPnefghx9EwRJfLB3/sXEf78OHL7yV6IMrvEVEAJCYs+3 +w/lspCMJC0hOomxnt0vjyCCd0JeaEwihQGbOo9V0reXzrUy8yNkwo1w8mMSbIvqh ++D5uTB0jKL/ml1EVLw3NJf68MYIBmjCCAZYCAQEwgYAwezELMAkGA1UEBhMCU0cx +ETAPBgNVBAoTCE0yQ3J5cHRvMRQwEgYDVQQLEwtNMkNyeXB0byBDQTEkMCIGA1UE +AxMbTTJDcnlwdG8gQ2VydGlmaWNhdGUgTWFzdGVyMR0wGwYJKoZIhvcNAQkBFg5u +Z3BzQHBvc3QxLmNvbQIBAjAJBgUrDgMCGgUAoIGxMBgGCSqGSIb3DQEJAzELBgkq +hkiG9w0BBwEwHAYJKoZIhvcNAQkFMQ8XDTAyMTIwODE3NTIyMVowIwYJKoZIhvcN +AQkEMRYEFI/KcwJXhIg0bRzYLfAtDhxRMzghMFIGCSqGSIb3DQEJDzFFMEMwCgYI +KoZIhvcNAwcwDgYIKoZIhvcNAwICAgCAMA0GCCqGSIb3DQMCAgFAMAcGBSsOAwIH +MA0GCCqGSIb3DQMCAgEoMA0GCSqGSIb3DQEBAQUABEBrhZ8JVqGZ+S7dh+xKDwbF +yRXiOQWEa2uxD1O5fD02VfGEzDSrV1sPdQ8AcM3o+ny5AyC11E4Fns2cIkXwZEwz + diff --git a/demo/smime/sendsmime.py b/demo/smime/sendsmime.py new file mode 100644 index 0000000..9cc446b --- /dev/null +++ b/demo/smime/sendsmime.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python + +"""S/MIME sender. + +Copyright (c) 2000 Ng Pheng Siong. All rights reserved.""" + +from M2Crypto import BIO, Rand, SMIME, X509 +import smtplib, string, sys + +def sendsmime(from_addr, to_addrs, subject, msg, from_key, from_cert=None, to_certs=None, smtpd='localhost'): + + msg_bio = BIO.MemoryBuffer(msg) + sign = from_key + encrypt = to_certs + + s = SMIME.SMIME() + if sign: + s.load_key(from_key, from_cert) + p7 = s.sign(msg_bio, flags=SMIME.PKCS7_TEXT) + msg_bio = BIO.MemoryBuffer(msg) # Recreate coz sign() has consumed it. + + if encrypt: + sk = X509.X509_Stack() + for x in to_certs: + sk.push(X509.load_cert(x)) + s.set_x509_stack(sk) + s.set_cipher(SMIME.Cipher('rc2_40_cbc')) + tmp_bio = BIO.MemoryBuffer() + if sign: + s.write(tmp_bio, p7) + else: + tmp_bio.write(msg) + p7 = s.encrypt(tmp_bio) + + out = BIO.MemoryBuffer() + out.write('From: %s\r\n' % from_addr) + out.write('To: %s\r\n' % string.join(to_addrs, ", ")) + out.write('Subject: %s\r\n' % subject) + if encrypt: + s.write(out, p7) + else: + if sign: + s.write(out, p7, msg_bio, SMIME.PKCS7_TEXT) + else: + out.write('\r\n') + out.write(msg) + out.close() + + smtp = smtplib.SMTP() + smtp.connect(smtpd) + smtp.sendmail(from_addr, to_addrs, out.read()) + smtp.quit() + + # XXX Cleanup the stack and store. + + +msg = """ +S/MIME - Secure Multipurpose Internet Mail Extensions [RFC 2311, RFC 2312] - +provides a consistent way to send and receive secure MIME data. Based on the +popular Internet MIME standard, S/MIME provides the following cryptographic +security services for electronic messaging applications - authentication, +message integrity and non-repudiation of origin (using digital signatures) +and privacy and data security (using encryption). + +S/MIME is built on the PKCS #7 standard. [PKCS7] + +S/MIME is implemented in Netscape Messenger and Microsoft Outlook. +""" + + +if __name__ == '__main__': + Rand.load_file('../randpool.dat', -1) + sendsmime(from_addr = 'ngps@post1.com', + to_addrs = ['jerry','ngps@post1.com'], + subject = 'S/MIME testing', + msg = msg, + from_key = 'client2.pem', + #from_key = None, + #to_certs = None) + to_certs = ['client.pem']) + Rand.save_file('../randpool.dat') + diff --git a/demo/smime/test.py b/demo/smime/test.py new file mode 100644 index 0000000..a59cf9f --- /dev/null +++ b/demo/smime/test.py @@ -0,0 +1,192 @@ +#!/usr/bin/env python + +"""S/MIME demo. + +Copyright (c) 2000 Ng Pheng Siong. All rights reserved.""" + +from M2Crypto import BIO, Rand, SMIME, X509 + +ptxt = """ +S/MIME - Secure Multipurpose Internet Mail Extensions [RFC 2311, RFC 2312] - +provides a consistent way to send and receive secure MIME data. Based on the +popular Internet MIME standard, S/MIME provides the following cryptographic +security services for electronic messaging applications - authentication, +message integrity and non-repudiation of origin (using digital signatures) +and privacy and data security (using encryption). + +S/MIME is built on the PKCS #7 standard. [PKCS7] + +S/MIME is implemented in Netscape Messenger and Microsoft Outlook. +""" + +def makebuf(): + buf = BIO.MemoryBuffer(ptxt) + return buf + +def sign(): + print 'test sign & save...', + buf = makebuf() + s = SMIME.SMIME() + s.load_key('client.pem') + p7 = s.sign(buf) + out = BIO.openfile('clear.p7', 'w') + out.write('To: ngps@post1.com\n') + out.write('From: ngps@post1.com\n') + out.write('Subject: testing\n') + buf = makebuf() # Recreate buf, because sign() has consumed it. + s.write(out, p7, buf) + out.close() + + buf = makebuf() + p7 = s.sign(buf) + out = BIO.openfile('opaque.p7', 'w') + out.write('To: ngps@post1.com\n') + out.write('From: ngps@mpost1.com\n') + out.write('Subject: testing\n') + s.write(out, p7) + out.close() + print 'ok' + +def verify_clear(): + print 'test load & verify clear...', + s = SMIME.SMIME() + x509 = X509.load_cert('client.pem') + sk = X509.X509_Stack() + sk.push(x509) + s.set_x509_stack(sk) + st = X509.X509_Store() + st.load_info('ca.pem') + s.set_x509_store(st) + p7, data = SMIME.smime_load_pkcs7('clear.p7') + v = s.verify(p7) + if v: + print 'ok' + else: + print 'not ok' + +def verify_opaque(): + print 'test load & verify opaque...', + s = SMIME.SMIME() + x509 = X509.load_cert('client.pem') + sk = X509.X509_Stack() + sk.push(x509) + s.set_x509_stack(sk) + st = X509.X509_Store() + st.load_info('ca.pem') + s.set_x509_store(st) + p7, data = SMIME.smime_load_pkcs7('opaque.p7') + v = s.verify(p7, data) + if v: + print 'ok' + else: + print 'not ok' + +def verify_netscape(): + print 'test load & verify netscape messager output...', + s = SMIME.SMIME() + #x509 = X509.load_cert('client.pem') + sk = X509.X509_Stack() + #sk.push(x509) + s.set_x509_stack(sk) + st = X509.X509_Store() + st.load_info('ca.pem') + s.set_x509_store(st) + p7, data = SMIME.smime_load_pkcs7('ns.p7') + v = s.verify(p7, data) + print '\n', v, '\n...ok' + + +def sv(): + print 'test sign/verify...', + buf = makebuf() + s = SMIME.SMIME() + + # Load a private key. + s.load_key('client.pem') + + # Sign. + p7 = s.sign(buf) + + # Output the stuff. + bio = BIO.MemoryBuffer() + s.write(bio, p7, buf) + + # Plumbing for verification: CA's cert. + st = X509.X509_Store() + st.load_info('ca.pem') + s.set_x509_store(st) + + # Plumbing for verification: Signer's cert. + x509 = X509.load_cert('client.pem') + sk = X509.X509_Stack() + sk.push(x509) + s.set_x509_stack(sk) + + # Verify. + p7, buf = SMIME.smime_load_pkcs7_bio(bio) + v = s.verify(p7, flags=SMIME.PKCS7_DETACHED) + + if v: + print 'ok' + else: + print 'not ok' + +def ed(): + print 'test encrypt/decrypt...', + buf = makebuf() + s = SMIME.SMIME() + + # Load target cert to encrypt to. + x509 = X509.load_cert('client.pem') + sk = X509.X509_Stack() + sk.push(x509) + s.set_x509_stack(sk) + + # Add a cipher. + s.set_cipher(SMIME.Cipher('bf_cbc')) + + # Encrypt. + p7 = s.encrypt(buf) + + # Load target's private key. + s.load_key('client.pem') + + # Decrypt. + data = s.decrypt(p7) + + if data: + print 'ok' + else: + print 'not ok' + + +def zope_test(): + print 'test zophistry...' + f = open('client.pem') + cert_str = f.read() + key_bio = BIO.MemoryBuffer(cert_str) + cert_bio = BIO.MemoryBuffer(cert_str) # XXX Kludge. + s = SMIME.SMIME() + s.load_key_bio(key_bio, cert_bio) + # XXX unfinished... + + +def leak_test(): + # Seems ok, not leaking. + while 1: + ed() + #sv() + + +if __name__ == '__main__': + Rand.load_file('../randpool.dat', -1) + ed() + sign() + verify_opaque() + verify_clear() + #verify_netscape() + sv() + #zope_test() + #leak_test() + Rand.save_file('../randpool.dat') + diff --git a/demo/smime/unsmime.py b/demo/smime/unsmime.py new file mode 100644 index 0000000..ffc40ad --- /dev/null +++ b/demo/smime/unsmime.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python + +"""S/MIME demo. + +Copyright (c) 2000 Ng Pheng Siong. All rights reserved.""" + +from M2Crypto import BIO, Rand, SMIME, X509 +import sys + +def decrypt_verify(p7file, recip_key, signer_cert, ca_cert): + s = SMIME.SMIME() + + # Load decryption private key. + s.load_key(recip_key) + + # Extract PKCS#7 blob from input. + p7, bio = SMIME.smime_load_pkcs7_bio(p7file) + + # Decrypt. + data = s.decrypt(p7) + + # Because we passed in a SignAndEnveloped blob, the output + # of our decryption is a Signed blob. We now verify it. + + # Load the signer's cert. + sk = X509.X509_Stack() + s.set_x509_stack(sk) + + # Load the CA cert. + st = X509.X509_Store() + st.load_info(ca_cert) + s.set_x509_store(st) + + # Verify. + p7, bio = SMIME.smime_load_pkcs7_bio(BIO.MemoryBuffer(data)) + if bio is not None: + # Netscape Messenger clear-signs, when also encrypting. + data = s.verify(p7, bio) + else: + # M2Crypto's sendsmime.py opaque-signs, when also encrypting. + data = s.verify(p7) + + print data + + +if __name__ == '__main__': + Rand.load_file('../randpool.dat', -1) + decrypt_verify(BIO.File(sys.stdin), 'client.pem', 'client2.pem','ca.pem') + Rand.save_file('../randpool.dat') + diff --git a/demo/ssl/README b/demo/ssl/README new file mode 100644 index 0000000..07e43b8 --- /dev/null +++ b/demo/ssl/README @@ -0,0 +1,25 @@ + 29 Nov 00 +----------- + +This directory contains SSL clients and servers written as demos +and testbeds. + +The oldest ones are s_server.py and s_client.py: these are modeled +after the eponymous demo programs in OpenSSL. Once I got them going, +I moved on to Medusa and other "real-world" servers. The pair has been +in a state of neglect and quite likely no longer work with the current +M2Crypto. + +My current testbeds are the echod-* servers and echo.py. These should +always work. Note that Python on the three platforms that I test on +are built --with-threads. + + +Python 2.0's httplib introduces HTTP/1.1 functionality and an interface +substantially different from Python 1.5.2's HTTP/1.0 interface. + +M2Crypto.httpslib provides both interfaces. The demo programs are +https_cli.py and urllib_cli.py; these have been tested with +Apache/1.3.14 (Unix) mod_ssl/2.7.1 OpenSSL/0.9.6. + + diff --git a/demo/ssl/c.py b/demo/ssl/c.py new file mode 100644 index 0000000..bd03e6e --- /dev/null +++ b/demo/ssl/c.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python + +"""C programming in Python. Have SWIG sweat the pointers. ;-) + +Copyright (c) 1999-2003 Ng Pheng Siong. All rights reserved.""" + +from socket import * +import sys + +from M2Crypto import SSL, m2 + +HOST = '127.0.0.1' +PORT = 9443 +req_10 = 'GET / HTTP/1.0\r\n\r\n' +req_11 = 'GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n' + + +def c_10(): + c_style(HOST, PORT, req_10) + + +def c_11(): + c_style(HOST, PORT, req_11) + + +def c_style(HOST, PORT, req): + + # Set up SSL context. + ctx = m2.ssl_ctx_new(m2.sslv3_method()) + m2.ssl_ctx_use_cert(ctx, 'client.pem') + m2.ssl_ctx_use_privkey(ctx, 'client.pem') + + # Make the socket connection. + s = socket(AF_INET, SOCK_STREAM) + s.connect((HOST, PORT)) + + # Set up the SSL connection. + sbio = m2.bio_new_socket(s.fileno(), 0) + ssl = m2.ssl_new(ctx) + m2.ssl_set_bio(ssl, sbio, sbio) + m2.ssl_connect(ssl) + sslbio = m2.bio_new(m2.bio_f_ssl()) + m2.bio_set_ssl(sslbio, ssl, 0) + + # Push a buffering BIO over the SSL BIO. + iobuf = m2.bio_new(m2.bio_f_buffer()) + topbio = m2.bio_push(iobuf, sslbio) + + # Send the request. + m2.bio_write(sslbio, req) + + # Receive the response. + while 1: + data = m2.bio_gets(topbio, 4096) + if not data: break + sys.stdout.write(data) + + # Cleanup. May be missing some necessary steps. ;-| + m2.bio_pop(topbio) + m2.bio_free(iobuf) + m2.ssl_shutdown(ssl) + m2.ssl_free(ssl) + m2.ssl_ctx_free(ctx) + s.close() + + +if __name__ == '__main__': + c_10() + c_11() + diff --git a/demo/ssl/c_bio.py b/demo/ssl/c_bio.py new file mode 100644 index 0000000..1c20e91 --- /dev/null +++ b/demo/ssl/c_bio.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python + +"""C programming in Python. Have SWIG sweat the pointers. ;-) + +Copyright (c) 1999-2000 Ng Pheng Siong. All rights reserved.""" + +from socket import * +import sys + +from M2Crypto import SSL, m2 + +HOST = '127.0.0.1' +PORT = 9443 +req_10 = 'GET / HTTP/1.0\r\n\r\n' +req_11 = 'GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n' + + +def c_10(): + c_style(HOST, PORT, req_10) + + +def c_11(): + c_style(HOST, PORT, req_11) + + +def c_style(HOST, PORT, req): + + # Set up SSL context. + ctx = m2.ssl_ctx_new(m2.sslv3_method()) + m2.ssl_ctx_use_cert(ctx, 'client.pem') + m2.ssl_ctx_use_privkey(ctx, 'client.pem') + + # Make the socket connection. + s = socket(AF_INET, SOCK_STREAM) + s.connect((HOST, PORT)) + + # Set up the SSL connection. + sbio = m2.bio_new_socket(s.fileno(), 0) + ssl = m2.ssl_new(ctx) + m2.ssl_set_bio(ssl, sbio, sbio) + m2.ssl_connect(ssl) + sslbio = m2.bio_new(m2.bio_f_ssl()) + m2.bio_set_ssl(sslbio, ssl, 0) + + # Push a buffering BIO over the SSL BIO. + iobuf = m2.bio_new(m2.bio_f_buffer()) + topbio = m2.bio_push(iobuf, sslbio) + + # Send the request. + m2.bio_write(sslbio, req) + + # Receive the response. + while 1: + data = m2.bio_gets(topbio, 4096) + if not data: break + sys.stdout.write(data) + + # Cleanup. May be missing some necessary steps. ;-| + m2.bio_pop(topbio) + m2.bio_free(iobuf) + m2.ssl_shutdown(ssl) + m2.ssl_free(ssl) + m2.ssl_ctx_free(ctx) + s.close() + + +if __name__ == '__main__': + #c_10() + c_11() + diff --git a/demo/ssl/ca.der b/demo/ssl/ca.der Binary files differnew file mode 100644 index 0000000..8f4ed80 --- /dev/null +++ b/demo/ssl/ca.der diff --git a/demo/ssl/ca.pem b/demo/ssl/ca.pem new file mode 100644 index 0000000..b7c84a1 --- /dev/null +++ b/demo/ssl/ca.pem @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDWTCCAsKgAwIBAgIBADANBgkqhkiG9w0BAQQFADCBgDELMAkGA1UEBhMCU0cx +ETAPBgNVBAoTCE0yQ3J5cHRvMRQwEgYDVQQLEwtNMkNyeXB0byBDQTEkMCIGA1UE +AxMbTTJDcnlwdG8gQ2VydGlmaWNhdGUgTWFzdGVyMSIwIAYJKoZIhvcNAQkBFhNu +Z3BzQG5ldG1lbWV0aWMuY29tMB4XDTAxMTIxNTA1NTU0NloXDTA0MTIxNDA1NTU0 +NlowgYAxCzAJBgNVBAYTAlNHMREwDwYDVQQKEwhNMkNyeXB0bzEUMBIGA1UECxML +TTJDcnlwdG8gQ0ExJDAiBgNVBAMTG00yQ3J5cHRvIENlcnRpZmljYXRlIE1hc3Rl +cjEiMCAGCSqGSIb3DQEJARYTbmdwc0BuZXRtZW1ldGljLmNvbTCBnzANBgkqhkiG +9w0BAQEFAAOBjQAwgYkCgYEAx8soJbS719LHK62VVVIQeC3oW0HvFArwPnA0LuEK +q+LaqMOJg1rS7hvFdX03diV+XJw7cC0iECZYJNG4ii1xbY6KRmufkInaAwm54E3N +e+YYVocaqUkcN6xVf6fwnLfPXbpFS/K2Umg11ObKMmi80JmiIdjcjRRCQZC7g1hf +q+kCAwEAAaOB4DCB3TAdBgNVHQ4EFgQU6/qcBzEtQphfXLhiOHbt2KqBwMIwga0G +A1UdIwSBpTCBooAU6/qcBzEtQphfXLhiOHbt2KqBwMKhgYakgYMwgYAxCzAJBgNV +BAYTAlNHMREwDwYDVQQKEwhNMkNyeXB0bzEUMBIGA1UECxMLTTJDcnlwdG8gQ0Ex +JDAiBgNVBAMTG00yQ3J5cHRvIENlcnRpZmljYXRlIE1hc3RlcjEiMCAGCSqGSIb3 +DQEJARYTbmdwc0BuZXRtZW1ldGljLmNvbYIBADAMBgNVHRMEBTADAQH/MA0GCSqG +SIb3DQEBBAUAA4GBAD+I14GuS5vJmyv1k7mUMbAicsWRHZ+zrGOq9L/L2LsA+lKQ +dAzEZE2+Zv8LBPJVltbJJhcFNJS/ZMAjEm4xlJuCpvXVMxd/M5AM29aqekWlIK7J +vsdDL8IuzpRkMniUiNKPhmB6IPIOslvUKx6QofcE0wDh6pg4VvIbCjkpZ7gf +-----END CERTIFICATE----- diff --git a/demo/ssl/client.p12 b/demo/ssl/client.p12 Binary files differnew file mode 100644 index 0000000..9c7a640 --- /dev/null +++ b/demo/ssl/client.p12 diff --git a/demo/ssl/client.pem b/demo/ssl/client.pem new file mode 100644 index 0000000..c0274ad --- /dev/null +++ b/demo/ssl/client.pem @@ -0,0 +1,35 @@ +-----BEGIN CERTIFICATE----- +MIIDPzCCAqigAwIBAgIBBTANBgkqhkiG9w0BAQQFADCBgDELMAkGA1UEBhMCU0cx +ETAPBgNVBAoTCE0yQ3J5cHRvMRQwEgYDVQQLEwtNMkNyeXB0byBDQTEkMCIGA1UE +AxMbTTJDcnlwdG8gQ2VydGlmaWNhdGUgTWFzdGVyMSIwIAYJKoZIhvcNAQkBFhNu +Z3BzQG5ldG1lbWV0aWMuY29tMB4XDTAzMDYyMjEzMzYyNFoXDTA0MDYyMTEzMzYy +NFowOjELMAkGA1UEBhMCU0cxETAPBgNVBAoTCE0yQ3J5cHRvMRgwFgYDVQQDEw9N +MkNyeXB0byBDbGllbnQwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAOrvRK8h +lTNZ6rSWfIR/DuNpq+gQ42FCem/ai/dLKCxWB2BMHLHVkfCtEsJpeltigIFgZ28E +1h9Dx8RjP8R3lbTdJhv37j+3iUUq5lNXrlIWoKkyJsI4c1gC5kXke2z4uloj3W1E +dWwOPjKxCi2OGKqwRxNzLrIlnBJ6WsP53PghAgMBAAGjggEMMIIBCDAJBgNVHRME +AjAAMCwGCWCGSAGG+EIBDQQfFh1PcGVuU1NMIEdlbmVyYXRlZCBDZXJ0aWZpY2F0 +ZTAdBgNVHQ4EFgQUMwLQdUUrkDAlEuZGt9fw/6N3mKswga0GA1UdIwSBpTCBooAU +6/qcBzEtQphfXLhiOHbt2KqBwMKhgYakgYMwgYAxCzAJBgNVBAYTAlNHMREwDwYD +VQQKEwhNMkNyeXB0bzEUMBIGA1UECxMLTTJDcnlwdG8gQ0ExJDAiBgNVBAMTG00y +Q3J5cHRvIENlcnRpZmljYXRlIE1hc3RlcjEiMCAGCSqGSIb3DQEJARYTbmdwc0Bu +ZXRtZW1ldGljLmNvbYIBADANBgkqhkiG9w0BAQQFAAOBgQBzJ2Q06/QM2f/OHBzo +T0QpqAdWFmuclFecuKkzXOdh93cuCpJBrxBsMJMhvW10c7M9kEjW8GEt+obv2prt +39WDZVejg83O2YUexJlUnhmA3Zc5izkOT0+VasIGSKgt/eQm9p6klk/LKcVEZGzh +mI26dvftjo0HisrW5zfwEjzbdA== +-----END CERTIFICATE----- +-----BEGIN RSA PRIVATE KEY----- +MIICXQIBAAKBgQDq70SvIZUzWeq0lnyEfw7jaavoEONhQnpv2ov3SygsVgdgTByx +1ZHwrRLCaXpbYoCBYGdvBNYfQ8fEYz/Ed5W03SYb9+4/t4lFKuZTV65SFqCpMibC +OHNYAuZF5Hts+LpaI91tRHVsDj4ysQotjhiqsEcTcy6yJZwSelrD+dz4IQIDAQAB +AoGBAL8VDRBEiE3T/IoVPAGoNjvRXvjJg6c/osYHQ4BHqM0my6kPPueFhcXzfyaR +E+vwGgUgnAA4NtAHGRwqfVsWyLNbeogKO6kEfGoLY+6wjxQrrLohSeS9EUyvqFzW +dOC082piwzo61OK6q3iLplHPuQ6RK10VR1PQVVtpF/OuaxaBAkEA+6HpW7JTZswu +WCxMkpT0mXBa6rEqVnkv6WiZyQoFNsqgsSotqifpcdH39mixji+sO9gaUI6uKtcU +6SSlKRCYzQJBAO8DKaKh9Dq8U+nngc+s9NrkvfDDD0zzdoMznPmV8qQj50sPoJZF +1nK0wMjaIRrKiMKceOHnFuJnpchKiZiEbKUCQQCk0QJ2azExjd91JV7qS+KCdhM2 +0eA3T51QNpE0GvobT1E9ebD7WLURNkRCA4T46sTXVc62oR33NXWe17/OS+6pAkAB +FjiYPrhHlBellqHmedjbLfMXJyvoo6rESfXKxL3HtUoV80o9pK+m8d92ildgMc+R +YvjBvjVCbko4sO4TPXbpAkAE53+iOITLFBeF3dTBh77LvDDHy4Z94knsWSzZv/aL +26Zvjl5iZjHK6ECIe35kXzVYCWOgghI4ixHSMWOV3bt4 +-----END RSA PRIVATE KEY----- diff --git a/demo/ssl/dh1024.pem b/demo/ssl/dh1024.pem new file mode 100644 index 0000000..81d43f6 --- /dev/null +++ b/demo/ssl/dh1024.pem @@ -0,0 +1,5 @@ +-----BEGIN DH PARAMETERS----- +MIGHAoGBAJf2QmHKtQXdKCjhPx1ottPb0PMTBH9A6FbaWMsTuKG/K3g6TG1Z1fkq +/Gz/PWk/eLI9TzFgqVAuPvr3q14a1aZeVUMTgo2oO5/y2UHe6VaJ+trqCTat3xlx +/mNbIK9HA2RgPC3gWfVLZQrY+gz3ASHHR5nXWHEyvpuZm7m3h+irAgEC +-----END DH PARAMETERS----- diff --git a/demo/ssl/echo-eg.py b/demo/ssl/echo-eg.py new file mode 100644 index 0000000..f7975d4 --- /dev/null +++ b/demo/ssl/echo-eg.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python + +"""Demo SSL client #1 for the HOWTO. + +Copyright (c) 1999-2001 Ng Pheng Siong. All rights reserved.""" + +import getopt, sys +from socket import gethostname +from M2Crypto import Err, Rand, SSL, X509, threading + +host = '127.0.0.1' +port = 9999 + +optlist, optarg = getopt.getopt(sys.argv[1:], 'h:p:') +for opt in optlist: + if '-h' in opt: + host = opt[1] + elif '-p' in opt: + port = int(opt[1]) + +Rand.load_file('../randpool.dat', -1) + +ctx = SSL.Context('sslv3') +ctx.load_cert('client.pem') +#ctx.load_verify_info('ca.pem') +ctx.set_verify(SSL.verify_peer, 10) +ctx.set_info_callback() + +s = SSL.Connection(ctx) +s.connect((host, port)) +print 'Host =', gethostname() +print 'Cipher =', s.get_cipher().name() + +peer = s.get_peer_cert() +print 'Server =', peer.get_subject().CN + +while 1: + data = s.recv() + if not data: + break + sys.stdout.write(data) + sys.stdout.flush() + buf = sys.stdin.readline() + if not buf: + break + s.send(buf) + +s.close() + +Rand.save_file('../randpool.dat') + diff --git a/demo/ssl/echo.py b/demo/ssl/echo.py new file mode 100644 index 0000000..912e248 --- /dev/null +++ b/demo/ssl/echo.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python + +"""A simple SSL 'echo' client. + +Copyright (c) 1999-2003 Ng Pheng Siong. All rights reserved.""" + +import getopt, sys +from socket import gethostname +from M2Crypto import Err, Rand, SSL, X509 + +host = '127.0.0.1' +port = 9999 + +optlist, optarg = getopt.getopt(sys.argv[1:], 'h:p:') +for opt in optlist: + if '-h' in opt: + host = opt[1] + elif '-p' in opt: + port = int(opt[1]) + +Rand.load_file('../randpool.dat', -1) + +ctx = SSL.Context('sslv3') +ctx.load_cert_chain('client.pem') +#ctx.set_verify(SSL.verify_none, 10) +ctx.set_verify(SSL.verify_peer, 10, SSL.cb.ssl_verify_callback) +ctx.load_verify_locations('ca.pem') +#ctx.set_allow_unknown_ca(1) +ctx.set_info_callback() + +s = SSL.Connection(ctx) +s.connect((host, port)) +print 'Host =', gethostname() +print 'Cipher =', s.get_cipher().name() + +## 2003-06-28, ngps: Depends on ctx.set_verify() above, RTFM for details. +## v = s.get_verify_result() +## if v != X509.V_OK: +## s.close() +## raise SystemExit, 'Server verification failed' + +peer = s.get_peer_cert() +print 'Server =', str(peer.get_subject()) + +while 1: + data = s.recv() + if not data: + break + sys.stdout.write(data) + sys.stdout.flush() + buf = sys.stdin.readline() + if not buf: + break + s.send(buf) + +s.close() + +Rand.save_file('../randpool.dat') + diff --git a/demo/ssl/echod-async.py b/demo/ssl/echod-async.py new file mode 100644 index 0000000..0881c76 --- /dev/null +++ b/demo/ssl/echod-async.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python + +"""An asyncore-based SSL 'echo' server. + +Copyright (c) 1999-2003 Ng Pheng Siong. All rights reserved.""" + +import asyncore, errno, socket, time +from M2Crypto import Rand, SSL +import echod_lib + +class ssl_echo_channel(asyncore.dispatcher): + + buffer = 'Ye Olde Echo Servre\r\n' + + def __init__(self, conn): + asyncore.dispatcher.__init__(self, conn) + self._ssl_accepting = 1 + self.peer = self.get_peer_cert() + + def handle_connect(self): + pass + + def handle_close(self): + self.close() + + def writable(self): + return self._ssl_accepting or (len(self.buffer) > 0) + + def handle_write(self): + if self._ssl_accepting: + s = self.socket.accept_ssl() + if s: + self._ssl_accepting = 0 + else: + try: + n = self.send(self.buffer) + if n == -1: + pass + elif n == 0: + self.handle_close() + else: + self.buffer = self.buffer[n:] + except SSL.SSLError, what: + if str(what) == 'unexpected eof': + self.handle_close() + return + else: + raise + + def readable(self): + return 1 + + def handle_read(self): + if self._ssl_accepting: + s = self.socket.accept_ssl() + if s: + self._ssl_accepting = 0 + else: + try: + blob = self.recv(4096) + if blob is None: + pass + elif blob == '': + self.handle_close() + else: + self.buffer = self.buffer + blob + except SSL.SSLError, what: + if str(what) == 'unexpected eof': + self.handle_close() + return + else: + raise + + +class ssl_echo_server(SSL.ssl_dispatcher): + + channel_class=ssl_echo_channel + + def __init__(self, addr, port, ssl_context): + SSL.ssl_dispatcher.__init__(self) + self.create_socket(ssl_context) + self.set_reuse_addr() + self.socket.setblocking(0) + self.bind((addr, port)) + self.listen(5) + self.ssl_ctx=ssl_context + + def handle_accept(self): + try: + sock, addr = self.socket.accept() + self.channel_class(sock) + except: + print '-'*40 + import traceback + traceback.print_exc() + print '-'*40 + return + + def writable(self): + return 0 + + +if __name__=='__main__': + Rand.load_file('../randpool.dat', -1) + ctx = echod_lib.init_context('sslv23', 'server.pem', 'ca.pem', \ + #SSL.verify_peer | SSL.verify_fail_if_no_peer_cert) + SSL.verify_none) + ctx.set_tmp_dh('dh1024.pem') + ssl_echo_server('', 9999, ctx) + asyncore.loop() + Rand.save_file('../randpool.dat') + diff --git a/demo/ssl/echod-eg1.py b/demo/ssl/echod-eg1.py new file mode 100644 index 0000000..87358b7 --- /dev/null +++ b/demo/ssl/echod-eg1.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python + +"""Demo SSL server #1 for the HOWTO. + +Copyright (c) 1999-2001 Ng Pheng Siong. All rights reserved.""" + +import SocketServer +from M2Crypto import Err, Rand, SSL, threading + +def init_context(protocol, dhpfile, certfile, cafile, verify, verify_depth=10): + ctx = SSL.Context(protocol) + ctx.set_tmp_dh(dhpfile) + ctx.load_cert(certfile) + #ctx.load_verify_info(cafile) + ctx.set_verify(verify, verify_depth) + ctx.set_session_id_ctx('echod') + ctx.set_info_callback() + return ctx + +class ssl_echo_handler(SocketServer.BaseRequestHandler): + + buffer = 'Ye Olde Echo Servre\r\n' + + def handle(self): + peer = self.request.get_peer_cert() + if peer is not None: + print 'Client CA =', peer.get_issuer().O + print 'Client Subject =', peer.get_subject().CN + self.request.write(self.buffer) + while 1: + buf = self.request.read() + if not buf: + break + self.request.write(buf) + + def finish(self): + self.request.set_shutdown(SSL.SSL_SENT_SHUTDOWN|SSL.SSL_RECEIVED_SHUTDOWN) + self.request.close() + +if __name__ == '__main__': + Rand.load_file('randpool.dat', -1) + threading.init() + ctx = init_context('sslv23', 'dh1024.pem', 'server.pem', 'ca.pem', SSL.verify_peer) + s = SSL.SSLServer(('', 9999), ssl_echo_handler, ctx) + s.serve_forever() + threading.cleanup() + Rand.save_file('randpool.dat') + + diff --git a/demo/ssl/echod-forking.py b/demo/ssl/echod-forking.py new file mode 100644 index 0000000..43faab3 --- /dev/null +++ b/demo/ssl/echod-forking.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python + +"""A forking SSL 'echo' server. + +Copyright (c) 1999-2003 Ng Pheng Siong. All rights reserved.""" + +from M2Crypto import DH, Rand, SSL +import echod_lib + +class ssl_echo_handler(echod_lib.ssl_echo_handler): + buffer = 'Ye Olde Forking Echo Servre\r\n' + + +if __name__ == '__main__': + Rand.load_file('../randpool.dat', -1) + ctx = echod_lib.init_context('sslv23', 'server.pem', 'ca.pem', + SSL.verify_peer | SSL.verify_fail_if_no_peer_cert) + ctx.set_tmp_dh('dh1024.pem') + s = SSL.ForkingSSLServer(('', 9999), ssl_echo_handler, ctx) + s.serve_forever() + Rand.save_file('../randpool.dat') + diff --git a/demo/ssl/echod-iterative.py b/demo/ssl/echod-iterative.py new file mode 100644 index 0000000..2e68699 --- /dev/null +++ b/demo/ssl/echod-iterative.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python + +"""A simple iterative SSL 'echo' server. + +Copyright (c) 1999-2003 Ng Pheng Siong. All rights reserved.""" + +from M2Crypto import Rand, SSL +import echod_lib + +class ssl_echo_handler(echod_lib.ssl_echo_handler): + buffer='Ye Olde One-At-A-Time Echo Servre\r\n' + + +if __name__=='__main__': + Rand.load_file('../randpool.dat', -1) + ctx=echod_lib.init_context('sslv23', 'server.pem', 'ca.pem', \ + SSL.verify_peer | SSL.verify_fail_if_no_peer_cert) + #SSL.verify_none) + ctx.set_tmp_dh('dh1024.pem') + s=SSL.SSLServer(('', 9999), ssl_echo_handler, ctx) + s.serve_forever() + Rand.save_file('../randpool.dat') + diff --git a/demo/ssl/echod-thread.py b/demo/ssl/echod-thread.py new file mode 100644 index 0000000..65db811 --- /dev/null +++ b/demo/ssl/echod-thread.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python + +"""Another multi-threading SSL 'echo' server. + +Copyright (c) 1999-2003 Ng Pheng Siong. All rights reserved.""" + +from M2Crypto import Rand, SSL, threading +import echod_lib + +import thread +from socket import * + +buffer='Ye Newe Threading Echo Servre\r\n' + +def echo_handler(sslctx, sock, addr): + sslconn = SSL.Connection(sslctx, sock) + sslconn.setup_addr(addr) + sslconn.setup_ssl() + sslconn.set_accept_state() + sslconn.accept_ssl() + sslconn.write(buffer) + while 1: + try: + buf = sslconn.read() + if not buf: + break + sslconn.write(buf) + except SSL.SSLError, what: + if str(what) == 'unexpected eof': + break + else: + raise + except: + break + # Threading servers need this. + sslconn.set_shutdown(SSL.SSL_RECEIVED_SHUTDOWN|SSL.SSL_SENT_SHUTDOWN) + sslconn.close() + + +if __name__=='__main__': + threading.init() + Rand.load_file('../randpool.dat', -1) + ctx=echod_lib.init_context('sslv23', 'server.pem', 'ca.pem', + SSL.verify_peer | SSL.verify_fail_if_no_peer_cert) + ctx.set_tmp_dh('dh1024.pem') + sock = socket(AF_INET, SOCK_STREAM) + sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) + sock.bind(('', 9999)) + sock.listen(5) + while 1: + conn, addr = sock.accept() + thread.start_new_thread(echo_handler, (ctx, conn, addr)) + Rand.save_file('../randpool.dat') + threading.cleanup() + diff --git a/demo/ssl/echod-threading.py b/demo/ssl/echod-threading.py new file mode 100644 index 0000000..81eaef3 --- /dev/null +++ b/demo/ssl/echod-threading.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python + +"""A multi-threading SSL 'echo' server. + +Copyright (c) 1999-2003 Ng Pheng Siong. All rights reserved.""" + +from M2Crypto import DH, Rand, SSL, threading +import echod_lib + +class ssl_echo_handler(echod_lib.ssl_echo_handler): + + buffer='Ye Olde Threading Echo Servre\r\n' + + def finish(self): + # Threading servers need this. + self.request.set_shutdown(SSL.SSL_SENT_SHUTDOWN|SSL.SSL_RECEIVED_SHUTDOWN) + self.request.close() + + +if __name__=='__main__': + try: + threading.init() + Rand.load_file('../randpool.dat', -1) + ctx=echod_lib.init_context('sslv23', 'server.pem', 'ca.pem', + SSL.verify_peer | SSL.verify_fail_if_no_peer_cert) + ctx.set_tmp_dh('dh1024.pem') + s=SSL.ThreadingSSLServer(('', 9999), ssl_echo_handler, ctx) + s.serve_forever() + Rand.save_file('../randpool.dat') + except: + threading.cleanup() + diff --git a/demo/ssl/echod_lib.py b/demo/ssl/echod_lib.py new file mode 100644 index 0000000..7bea683 --- /dev/null +++ b/demo/ssl/echod_lib.py @@ -0,0 +1,45 @@ +"""Support routines for the various SSL 'echo' servers. + +Copyright (c) 1999-2003 Ng Pheng Siong. All rights reserved.""" + +import SocketServer +from M2Crypto import SSL + +def init_context(protocol, certfile, cafile, verify, verify_depth=10): + ctx = SSL.Context(protocol) + ctx.load_cert_chain(certfile) + ctx.load_verify_locations(cafile) + ctx.set_client_CA_list_from_file(cafile) + ctx.set_verify(verify, verify_depth) + #ctx.set_allow_unknown_ca(1) + ctx.set_session_id_ctx('echod') + ctx.set_info_callback() + return ctx + + +class ssl_echo_handler(SocketServer.BaseRequestHandler): + + buffer = 'Ye Olde Echo Servre\r\n' + + def handle(self): + peer = self.request.get_peer_cert() + if peer is not None: + print 'Client CA =', peer.get_issuer().O + print 'Client Subject =', peer.get_subject().CN + x = self.request.write(self.buffer) + while 1: + try: + buf = self.request.read() + if not buf: + break + self.request.write(buf) + except SSL.SSLError, what: + if str(what) == 'unexpected eof': + break + else: + raise + + def finish(self): + self.request.close() + + diff --git a/demo/ssl/ftp_tls.py b/demo/ssl/ftp_tls.py new file mode 100644 index 0000000..163d87c --- /dev/null +++ b/demo/ssl/ftp_tls.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python + +"""Demo for M2Crypto.ftpslib's FTP/TLS client. + +This client interoperates with M2Crypto's Medusa-based FTP/TLS +server as well as Peter Runestig's patched-for-TLS OpenBSD FTP +server. + +Copyright (c) 1999-2004 Ng Pheng Siong. All rights reserved.""" + +from M2Crypto import SSL, ftpslib, threading + +def passive(): + ctx = SSL.Context('sslv23') + f = ftpslib.FTP_TLS(ssl_ctx=ctx) + f.connect('127.0.0.1', 39021) + f.auth_tls() + f.set_pasv(1) + f.login('ftp', 'ngps@') + f.prot_p() + f.retrlines('LIST') + f.quit() + +def active(): + ctx = SSL.Context('sslv23') + f = ftpslib.FTP_TLS(ssl_ctx=ctx) + f.connect('127.0.0.1', 39021) + f.auth_tls() + f.set_pasv(0) + f.login('ftp', 'ngps@') + f.prot_p() + f.retrlines('LIST') + f.quit() + + +if __name__ == '__main__': + threading.init() + active() + passive() + threading.cleanup() + diff --git a/demo/ssl/http_cli_20.py b/demo/ssl/http_cli_20.py new file mode 100644 index 0000000..0ce4cef --- /dev/null +++ b/demo/ssl/http_cli_20.py @@ -0,0 +1,22 @@ +import httplib, sys + +def test_httplib(): + h = httplib.HTTPConnection('127.0.0.1', 80) + h.set_debuglevel(1) + h.putrequest('GET', '/') + h.putheader('Accept', 'text/html') + h.putheader('Accept', 'text/plain') + h.putheader('Connection', 'close') + h.endheaders() + resp = h.getresponse() + f = resp.fp + while 1: + data = f.readline() + if not data: + break + sys.stdout.write(data) + f.close() + h.close() + +if __name__=='__main__': + test_httplib() diff --git a/demo/ssl/https_cli.py b/demo/ssl/https_cli.py new file mode 100644 index 0000000..543ba8f --- /dev/null +++ b/demo/ssl/https_cli.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python + +"""Demonstrations of M2Crypto.httpslib. + +Copyright (c) 1999-2003 Ng Pheng Siong. All rights reserved.""" + +import sys +from M2Crypto import Rand, SSL, httpslib, threading + + +def test_httpslib(): + ctx = SSL.Context('sslv23') + ctx.load_cert_chain('client.pem') + ctx.load_verify_locations('ca.pem', '') + ctx.set_verify(SSL.verify_peer, 10) + ctx.set_info_callback() + h = httpslib.HTTPSConnection('localhost', 19443, ssl_context=ctx) + h.set_debuglevel(1) + h.putrequest('GET', '/') + h.putheader('Accept', 'text/html') + h.putheader('Accept', 'text/plain') + h.putheader('Connection', 'close') + h.endheaders() + resp = h.getresponse() + f = resp.fp + c = 0 + while 1: + # Either of following two works. + #data = f.readline(4096) + data = resp.read(4096) + if not data: break + c = c + len(data) + #print data + sys.stdout.write(data) + sys.stdout.flush() + f.close() + h.close() + +if __name__=='__main__': + Rand.load_file('../randpool.dat', -1) + #threading.init() + test_httpslib() + #threading.cleanup() + Rand.save_file('../randpool.dat') + diff --git a/demo/ssl/https_cli_async.py b/demo/ssl/https_cli_async.py new file mode 100644 index 0000000..9e7a5d0 --- /dev/null +++ b/demo/ssl/https_cli_async.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python + +"""Demo for client-side ssl_dispatcher usage. Note that connect() +is blocking. (Need fix?) + +This isn't really a HTTPS client; it's just a toy. + +Copyright (c) 1999-2003 Ng Pheng Siong. All rights reserved.""" + +import asyncore, sys, time +from M2Crypto import Rand, SSL + +class https_client(SSL.ssl_dispatcher): + + def __init__(self, host, path, ssl_ctx): + SSL.ssl_dispatcher.__init__(self) + self.path = path + self.buffer = 'GET %s HTTP/1.0\r\n\r\n' % self.path + self.create_socket(ssl_ctx) + self.socket.connect((host, 19443)) + self._can_read = 1 + self._count = 0 + + def handle_connect(self): + pass + + def readable(self): + return self._can_read + + def handle_read(self): + try: + result = self.recv() + if result is None: + return + elif result == '': + self._can_read = 0 + sys.stdout.write('%s: total: %5d\n' % (self.path, self._count,)) + sys.stdout.flush() + self.close() + else: + #print result + l = len(result) + self._count = self._count + l + display = (time.time(), l, self.path) + sys.stdout.write('%14.3f: read %5d from %s\n' % display) + sys.stdout.flush() + except SSL.SSLError, why: + print 'handle_read:', why + self.close() + raise + + def writable(self): + return (len(self.buffer) > 0) + + def handle_write(self): + try: + sent = self.send(self.buffer) + self.buffer = self.buffer[sent:] + except SSL.SSLError, why: + print 'handle_write:', why + self.close() + + +if __name__ == '__main__': + Rand.load_file('../randpool.dat', -1) + ctx = SSL.Context() + url = ('/jdk118/api/u-names.html', + '/postgresql/xfunc-c.html', + '/python2.1/modindex.html') + for u in url: + https_client('localhost', u, ctx) + asyncore.loop() + Rand.save_file('../randpool.dat') + + +# Here's a sample output. Server is Apache+mod_ssl on localhost. +# $ python https_cli_async.py +# 991501090.682: read 278 from /python2.1/modindex.html +# 991501090.684: read 278 from /postgresql/xfunc-c.html +# 991501090.742: read 4096 from /postgresql/xfunc-c.html +# 991501090.743: read 4096 from /postgresql/xfunc-c.html +# 991501090.744: read 4096 from /postgresql/xfunc-c.html +# 991501090.744: read 4096 from /postgresql/xfunc-c.html +# 991501090.755: read 4096 from /postgresql/xfunc-c.html +# 991501090.756: read 278 from /jdk118/api/u-names.html +# 991501090.777: read 4096 from /postgresql/xfunc-c.html +# 991501090.778: read 4096 from /postgresql/xfunc-c.html +# 991501090.778: read 4096 from /postgresql/xfunc-c.html +# 991501090.782: read 4096 from /postgresql/xfunc-c.html +# 991501090.813: read 4096 from /python2.1/modindex.html +# 991501090.839: read 4096 from /jdk118/api/u-names.html +# 991501090.849: read 4096 from /python2.1/modindex.html +# 991501090.873: read 3484 from /postgresql/xfunc-c.html +# 991501090.874: read 4096 from /jdk118/api/u-names.html +# 991501090.874: read 4096 from /python2.1/modindex.html +#/postgresql/xfunc-c.html: total: 40626 +# 991501090.886: read 4096 from /jdk118/api/u-names.html +# 991501090.886: read 2958 from /python2.1/modindex.html +# 991501090.887: read 4096 from /jdk118/api/u-names.html +#/python2.1/modindex.html: total: 15524 +# 991501090.893: read 4096 from /jdk118/api/u-names.html +# 991501090.894: read 2484 from /jdk118/api/u-names.html +#/jdk118/api/u-names.html: total: 23242 +# $ + + diff --git a/demo/ssl/https_srv.py b/demo/ssl/https_srv.py new file mode 100644 index 0000000..6d12d80 --- /dev/null +++ b/demo/ssl/https_srv.py @@ -0,0 +1,150 @@ +"""This server extends BaseHTTPServer and SimpleHTTPServer thusly: +1. One thread per connection. +2. Generates directory listings. + +In addition, it has the following properties: +1. Works over HTTPS only. +2. Displays SSL handshaking and SSL session info. +3. Performs SSL renegotiation when a magic url is requested. + +TODO: +1. Cache stat() of directory entries. +2. Fancy directory indexing. +3. Interface ZPublisher. + +Copyright (c) 1999-2003 Ng Pheng Siong. All rights reserved. +""" + +import os, sys +from SimpleHTTPServer import SimpleHTTPRequestHandler + +from M2Crypto import Rand, SSL +from M2Crypto.SSL.SSLServer import ThreadingSSLServer + +try: + from cStringIO import StringIO +except ImportError: + from StringIO import StringIO + + +def mkdirlist(path, url): + dirlist = os.listdir(path) + dirlist.sort() + f = StringIO() + f.write('<title>Index listing for %s</title>\r\n' % (url,)) + f.write('<h1>Index listing for %s</h1>\r\n' % (url,)) + f.write('<pre>\r\n') + for d in dirlist: + if os.path.isdir(os.path.join(path, d)): + d2 = d + '/' + else: + d2 = d + if url == '/': + f.write('<a href="/%s">%s</a><br>\r\n' % (d, d2)) + else: + f.write('<a href="%s/%s">%s</a><br>\r\n' % (url, d, d2)) + f.write('</pre>\r\n\r\n') + f.reset() + return f + + +class HTTP_Handler(SimpleHTTPRequestHandler): + + server_version = "https_srv/0.1" + reneg = 0 + + # Cribbed from SimpleHTTPRequestHander to add the ".der" entry, + # which facilitates installing your own certificates into browsers. + extensions_map = { + '': 'text/plain', # Default, *must* be present + '.html': 'text/html', + '.htm': 'text/html', + '.gif': 'image/gif', + '.jpg': 'image/jpeg', + '.jpeg': 'image/jpeg', + '.der': 'application/x-x509-ca-cert' + } + + def send_head(self): + if self.path[1:8] == '_reneg_': + self.reneg = 1 + self.path = self.path[8:] + path = self.translate_path(self.path) + if os.path.isdir(path): + f = mkdirlist(path, self.path) + filetype = 'text/html' + else: + try: + f = open(path, 'rb') + filetype = self.guess_type(path) + except IOError: + self.send_error(404, "File not found") + return None + self.send_response(200) + self.send_header("Content-type", filetype) + self.end_headers() + return f + + def do_GET(self): + #sess = self.request.get_session() + #self.log_message('\n%s', sess.as_text()) + f = self.send_head() + if self.reneg: + self.reneg = 0 + self.request.renegotiate() + sess = self.request.get_session() + self.log_message('\n%s', sess.as_text()) + if f: + self.copyfile(f, self.wfile) + f.close() + + def do_HEAD(self): + #sess = self.request.get_session() + #self.log_message('\n%s', sess.as_text()) + f = self.send_head() + if f: + f.close() + + +class HTTPS_Server(ThreadingSSLServer): + def __init__(self, server_addr, handler, ssl_ctx): + ThreadingSSLServer.__init__(self, server_addr, handler, ssl_ctx) + self.server_name = server_addr[0] + self.server_port = server_addr[1] + + def finish(self): + self.request.set_shutdown(SSL.SSL_RECEIVED_SHUTDOWN | SSL.SSL_SENT_SHUTDOWN) + self.request.close() + + +def init_context(protocol, certfile, cafile, verify, verify_depth=10): + ctx=SSL.Context(protocol) + ctx.load_cert(certfile) + ctx.load_client_ca(cafile) + ctx.load_verify_info(cafile) + ctx.set_verify(verify, verify_depth) + ctx.set_allow_unknown_ca(1) + ctx.set_session_id_ctx('https_srv') + ctx.set_info_callback() + return ctx + + +if __name__ == '__main__': + from M2Crypto import threading as m2threading + m2threading.init() + if len(sys.argv) < 2: + wdir = '.' + else: + wdir = sys.argv[1] + Rand.load_file('../randpool.dat', -1) + ctx = init_context('sslv23', 'server.pem', 'ca.pem', \ + SSL.verify_none) + #SSL.verify_peer | SSL.verify_fail_if_no_peer_cert) + ctx.set_tmp_dh('dh1024.pem') + os.chdir(wdir) + httpsd = HTTPS_Server(('', 19443), HTTP_Handler, ctx) + httpsd.serve_forever() + Rand.save_file('../randpool.dat') + m2threading.cleanup() + + diff --git a/demo/ssl/myapp.py b/demo/ssl/myapp.py new file mode 100644 index 0000000..bb024d8 --- /dev/null +++ b/demo/ssl/myapp.py @@ -0,0 +1,30 @@ +""" +Sample application for socklib and somelib. + +Copyright (c) 2007 Open Source Applications Foundation. +All rights reserved. +""" + +# Sample application that uses socklib to override socket.ssl in order +# to make the 3rd party library use M2Crypto for SSL, instead of python +# stdlib SSL. + +import socklib # sets M2Crypto.SSL.Connection as socket.ssl + +# Set up the secure context for socklib +from M2Crypto import SSL + +def getContext(): + ctx = SSL.Context() + ctx.set_verify(SSL.verify_peer | SSL.verify_fail_if_no_peer_cert, 9) + ctx.load_verify_locations('ca.pem') + return ctx + +socklib.setSSLContextFactory(getContext) + +# Import and use 3rd party lib +import somelib + +if __name__ == '__main__': + c = somelib.HttpsGetSlash() + c.get('verisign.com', 443) diff --git a/demo/ssl/s_client.py b/demo/ssl/s_client.py new file mode 100644 index 0000000..021e3db --- /dev/null +++ b/demo/ssl/s_client.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python + +"""An M2Crypto implementation of OpenSSL's s_client. + +Copyright (c) 1999-2003 Ng Pheng Siong. All rights reserved.""" + +from socket import * +import getopt +import string +import sys + +from M2Crypto import SSL + +# s_server -www +HOST='127.0.0.1' +PORT=4433 +REQ='GET / HTTP/1.0\r\n\r\n' + +class Config: + pass + +def config(args): + options=['connect=', 'verify=', 'cert=', 'key=', 'CApath=', 'CAfile=', \ + 'reconnect', 'pause', 'showcerts', 'debug', 'nbio_test', 'state', \ + 'nbio', 'crlf', 'sslv2', 'sslv3', 'tlsv1', 'no_sslv2', 'no_sslv3', \ + 'no_tlsv1', 'bugs', 'cipher=', 'Verify='] + optlist, optarg=getopt.getopt(args, '', options) + + cfg=Config() + for opt in optlist: + setattr(cfg, opt[0][2:], opt[1]) + for x in (('tlsv1','no_tlsv1'),('sslv3','no_sslv3'),('sslv2','no_sslv2')): + if hasattr(cfg, x[0]) and hasattr(cfg, x[1]): + raise ValueError, 'mutually exclusive: %s and %s' % x + + if hasattr(cfg, 'connect'): + (host, port)=string.split(cfg.connect, ':') + cfg.connect=(host, int(port)) + else: + cfg.connect=(HOST, PORT) + + cfg.protocol=[] + # First protocol found will be used. + # Permutate the following tuple for preference. + for p in ('tlsv1', 'sslv3', 'sslv2'): + if hasattr(cfg, p): + cfg.protocol.append(p) + cfg.protocol.append('sslv23') + + return cfg + +def make_context(config): + ctx=SSL.Context(config.protocol[0]) + if hasattr(config, 'cert'): + cert=config.cert + else: + cert='client.pem' + if hasattr(config, 'key'): + key=config.key + else: + key='client.pem' + #ctx.load_cert(cert, key) + + if hasattr(config, 'verify'): + verify=SSL.verify_peer + depth=int(config.verify) + elif hasattr(config, 'Verify'): + verify=SSL.verify_peer | SSL.verify_fail_if_no_peer_cert + depth=int(config.Verify) + else: + verify=SSL.verify_none + depth=10 + config.verify=verify + config.verify_depth=depth + ctx.set_verify(verify, depth) + + if hasattr(config, 'CAfile'): + cafile=config.CAfile + else: + cafile='ca.pem' + ctx.load_verify_location(cafile) + + return ctx + +def s_client(config): + ctx=make_context(config) + s=SSL.Connection(ctx) + s.connect(config.connect) + if config.verify != SSL.verify_none and not s.verify_ok(): + print 'peer verification failed' + peer=s.get_peer_cert() + if peer is None: + print 'unable to get peer certificate' + else: + print 'peer.as_text()' + raise SystemExit + s.send(REQ) + while 1: + data=s.recv() + if not data: + break + print data + s.close() + +if __name__=='__main__': + cfg=config(sys.argv[1:]) + s_client(cfg) + diff --git a/demo/ssl/s_server.py b/demo/ssl/s_server.py new file mode 100644 index 0000000..84789a1 --- /dev/null +++ b/demo/ssl/s_server.py @@ -0,0 +1,203 @@ +#!/usr/bin/env python + +"""An M2Crypto implementation of OpenSSL's s_server. + +Copyright (c) 1999-2003 Ng Pheng Siong. All rights reserved.""" + +from socket import * +import asyncore +import cStringIO +import getopt +import string +import sys + +from M2Crypto import SSL, BIO, DH, Err + +# s_server -www +HOST='' +PORT=4433 + +class Config: + pass + +def config(args): + options=['accept=', 'context=', 'verify=', 'Verify=', 'cert=', 'key=', \ + 'dcert=', 'dkey=', 'nocert', 'crlf', 'debug', 'CApath=', 'CAfile=', \ + 'quiet', 'no_tmp_rsa', 'state', 'sslv2', 'sslv3', 'tlsv1', \ + 'no_sslv2', 'no_sslv3', 'no_tlsv1', 'bugs', 'cipher='] + optlist, optarg=getopt.getopt(args, '', options) + + cfg=Config() + for opt in optlist: + setattr(cfg, opt[0][2:], opt[1]) + for x in (('tlsv1','no_tlsv1'),('sslv3','no_sslv3'),('sslv2','no_sslv2')): + if hasattr(cfg, x[0]) and hasattr(cfg, x[1]): + raise ValueError, 'mutually exclusive: %s and %s' % x + + if hasattr(cfg, 'accept'): + cfg.accept=string.split(cfg.connect, ':') + else: + cfg.accept=(HOST, PORT) + + cfg.protocol=[] + # First protocol found will be used. + # Permutate the following tuple for preference. + for p in ('tlsv1', 'sslv3', 'sslv2'): + if hasattr(cfg, p): + cfg.protocol.append(p) + cfg.protocol.append('sslv23') + + return cfg + +RESP_HEAD="""\ +HTTP/1.0 200 ok +Content-type: text/html + +<HTML><BODY BGCOLOR=\"#ffffff\"> +<pre> + +Emulating s_server -www +Ciphers supported in s_server.py +""" + +RESP_TAIL="""\ +</pre> +</BODY></HTML> +""" + +class channel(SSL.ssl_dispatcher): + + def __init__(self, conn, debug): + SSL.ssl_dispatcher.__init__(self, conn) + self.socket.setblocking(0) + self.buffer=self.fixup_buffer() + self.debug=debug + + def fixup_buffer(self): + even=0 + buffer=cStringIO.StringIO() + buffer.write(RESP_HEAD) + for c in self.get_ciphers(): + # This formatting works for around 80 columns. + buffer.write('%-11s:%-28s' % (c.version(), c.name())) + if even: + buffer.write('\r\n') + even=1-even + buffer.write('\r\n%s' % RESP_TAIL) + return buffer.getvalue() + + def handle_connect(self): + pass + + def handle_close(self): + self.close() + + def handle_error(self, exc_type, exc_value, exc_traceback): + if self.debug: + print 'handle_error()' + #print exc_type, exc_value, exc_traceback + print Err.get_error() + self.handle_close() + + + def writeable(self): + return len(self.buffer) + + def handle_write(self): + n=self.send(self.buffer) + if n==-1: + pass + elif n==0: + self.handle_close() + else: + self.buffer=self.buffer[n:] + if self.debug: + print 'handle_write():', n + + def readable(self): + return 1 + + def handle_read(self): + blob=self.recv() + if blob is None: + pass + elif blob=='': + self.handle_close() + else: + pass + if self.debug: + print 'handle_read():', blob + + +class server(SSL.ssl_dispatcher): + + channel_class=channel + + def __init__(self, addr, port, config, ssl_context): + asyncore.dispatcher.__init__(self) + self.create_socket(ssl_context) + self.set_reuse_addr() + self.socket.setblocking(0) + self.bind((addr, port)) + self.listen(5) + self.config=config + self.debug=config.debug + self.ssl_ctx=ssl_context + + def handle_accept(self): + sock, addr=self.accept() + print self.ssl_ctx.get_verify_mode() + if (self.ssl_ctx.get_verify_mode() is SSL.verify_none) or sock.verify_ok(): + self.channel_class(sock, self.debug) + else: + print 'client verification failed' + sock.close() + + def writeable(self): + return 0 + +def s_server(config): + ctx=SSL.Context(config.protocol[0]) + + if hasattr(config, 'debug'): + config.debug=1 + else: + config.debug=0 + + if hasattr(config, 'cert'): + cert=config.cert + else: + cert='server.pem' + if hasattr(config, 'key'): + cert=config.key + else: + cert='server.pem' + ctx.load_cert(cert) + + if hasattr(config, 'CAfile'): + cafile=config.CAfile + else: + cafile='ca.pem' + ctx.load_verify_location(cafile) + + if hasattr(config, 'verify'): + verify=SSL.verify_peer + depth=int(config.verify) + elif hasattr(config, 'Verify'): + verify=SSL.verify_peer | SSL.verify_fail_if_no_peer_cert + depth=int(config.Verify) + else: + verify=SSL.verify_none + depth=0 + ctx.set_verify(verify, depth) + + ctx.set_tmp_dh('dh1024.pem') + #ctx.set_info_callback() + + server(cfg.accept[0], cfg.accept[1], cfg, ctx) + asyncore.loop() + +if __name__=='__main__': + cfg=config(sys.argv[1:]) + s_server(cfg) + diff --git a/demo/ssl/server.pem b/demo/ssl/server.pem new file mode 100644 index 0000000..1ee9282 --- /dev/null +++ b/demo/ssl/server.pem @@ -0,0 +1,36 @@ +-----BEGIN CERTIFICATE----- +MIIDYjCCAsugAwIBAgIBBDANBgkqhkiG9w0BAQQFADCBgDELMAkGA1UEBhMCU0cx +ETAPBgNVBAoTCE0yQ3J5cHRvMRQwEgYDVQQLEwtNMkNyeXB0byBDQTEkMCIGA1UE +AxMbTTJDcnlwdG8gQ2VydGlmaWNhdGUgTWFzdGVyMSIwIAYJKoZIhvcNAQkBFhNu +Z3BzQG5ldG1lbWV0aWMuY29tMB4XDTAzMDYyMjEzMzAxNFoXDTA0MDYyMTEzMzAx +NFowXTELMAkGA1UEBhMCU0cxETAPBgNVBAoTCE0yQ3J5cHRvMRIwEAYDVQQDEwls +b2NhbGhvc3QxJzAlBgkqhkiG9w0BCQEWGGFkbWluQHNlcnZlci5leGFtcGxlLmRv +bTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA37aKGZtFicl8xXXTLJ8/JD7c +kd3t/teCX9i61lpaDQCKBoVrrursIvZihemMKI9g/u/+BLqt5g8mBdgUdYz0txc8 +KEbV2hj+wwOX4H3XwD0Y+DysXiNHq7/tFdmzSVHoLxpY4zYzXbxQ/p049wvIyPRp +/y3omcnx/TEUhkn+JmkCAwEAAaOCAQwwggEIMAkGA1UdEwQCMAAwLAYJYIZIAYb4 +QgENBB8WHU9wZW5TU0wgR2VuZXJhdGVkIENlcnRpZmljYXRlMB0GA1UdDgQWBBTB +H/mUYlww24mJlSxEtGdlwojO9zCBrQYDVR0jBIGlMIGigBTr+pwHMS1CmF9cuGI4 +du3YqoHAwqGBhqSBgzCBgDELMAkGA1UEBhMCU0cxETAPBgNVBAoTCE0yQ3J5cHRv +MRQwEgYDVQQLEwtNMkNyeXB0byBDQTEkMCIGA1UEAxMbTTJDcnlwdG8gQ2VydGlm +aWNhdGUgTWFzdGVyMSIwIAYJKoZIhvcNAQkBFhNuZ3BzQG5ldG1lbWV0aWMuY29t +ggEAMA0GCSqGSIb3DQEBBAUAA4GBAAvl6v0s3eFeGP4iAcrfysuK7jzFKhjDYuOy +lVS3u33bZNLnMpM6OSEM9yPh4WpFCVHf+nYwC71pk4ilsLVXjKxymm2lNGcxLVuW +iydFz4Ly9nmN7Ja9ygYT39dGAFP/wN7ELTpsbul8VfmqhNg9y81d8i/A1tK3AGA8 +0QkPQNdP +-----END CERTIFICATE----- +-----BEGIN RSA PRIVATE KEY----- +MIICXAIBAAKBgQDftooZm0WJyXzFddMsnz8kPtyR3e3+14Jf2LrWWloNAIoGhWuu +6uwi9mKF6Ywoj2D+7/4Euq3mDyYF2BR1jPS3FzwoRtXaGP7DA5fgfdfAPRj4PKxe +I0erv+0V2bNJUegvGljjNjNdvFD+nTj3C8jI9Gn/LeiZyfH9MRSGSf4maQIDAQAB +AoGAHpeVtv62uarl9xKvuBBm0AwQmZnhq9HIsFaw5hMg8Vo7hbzFBvx1IirTOkC/ +u+QvfW1QLVFh6m3z4ySzV4fZBtcd6F9SbSrZ0xsxIUB2NOSa1RGgiaP61bJnMMM1 +xM3O9iwM5GZc3Gqy08QOCpDl0772VJ+9Gz3FA88mrc6rHQkCQQDz6RIatFjT28n8 +1vy0nHwwZz2oXTpe/pyZPwoKj8zVsmrKhKwOw7l8ArxjP8zoHOE7AQBCXYDMNoFp +IAF0yuqrAkEA6s0wMEdPpQeb0XHAfccyJQoeULxHdVFoz1wWmGSOm4YmQtR8/QJx +luEgfpeRkzxBKt5Ls3MEkheIOw7xV24zOwJAMz+DaE0AZPNHS3K4ghJnHZxzng6I +lzEUIjbWm0V/ml70hTy/EhMZw+6nOotLOHHo+QbK0SboSwAgzL/Gzo1cJQJANqpS +38qadleRJXAQWrg3qnvyluVe1aeAeVZ9RDmVIgxXeBO0jcs12uTLBe4PzHGo0mwy +v7K1i7XC180gzzQu5QJBAOxITT9RoWSSozPvnirHd37sn+RsrNYkV07NAa80M20Z +DkBPHeMVkNgigrQ6L6vWmbRDGQbGcMplAxnI5ppKCoU= +-----END RSA PRIVATE KEY----- diff --git a/demo/ssl/server3.py b/demo/ssl/server3.py new file mode 100644 index 0000000..99fbe97 --- /dev/null +++ b/demo/ssl/server3.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python +""" +server3 from the book 'Network Security with OpenSSL', but modified to +Python/M2Crypto from the original C implementation. + +Copyright (c) 2004-2005 Open Source Applications Foundation. +Author: Heikki Toivonen +""" +from M2Crypto import SSL, Rand, threading, DH +import thread +from socket import * + +verbose_debug = 1 + +def verify_callback(ok, store): + if not ok: + print "***Verify Not ok" + return ok + +dh1024 = None + +def init_dhparams(): + global dh1024 + dh1024 = DH.load_params('dh1024.pem') + +def tmp_dh_callback(ssl, is_export, keylength): + global dh1024 + if not dh1024: + init_dhparams() + return dh1024._ptr() + +def setup_server_ctx(): + ctx = SSL.Context('sslv23') + if ctx.load_verify_locations('ca.pem') != 1: + print "***No CA file" + #if ctx.set_default_verify_paths() != 1: + # print "***No default verify paths" + ctx.load_cert_chain('server.pem') + ctx.set_verify(SSL.verify_peer | SSL.verify_fail_if_no_peer_cert, + 10, verify_callback) + ctx.set_options(SSL.op_all | SSL.op_no_sslv2) + ctx.set_tmp_dh_callback(tmp_dh_callback) + #ctx.set_tmp_dh('dh1024.pem') + if ctx.set_cipher_list('ALL:!ADH:!LOW:!EXP:!MD5:@STRENGTH') != 1: + print "***No valid ciphers" + if verbose_debug: + ctx.set_info_callback() + return ctx + +def post_connection_check(peerX509, expectedHost): + if peerX509 is None: + print "***No peer certificate" + # Not sure if we can do any other checks + return 1 + +def do_server_loop(conn): + while 1: + try: + buf = conn.read() + if not buf: + break + print buf + except SSL.SSLError, what: + if str(what) == 'unexpected eof': + break + else: + raise + except: + break + + if conn.get_shutdown(): + return 1 + return 0 + +# How about something like: +#def server_thread(ctx, ssl, addr): +# conn = SSL.Connection(ctx, None) +# conn.ssl = ssl +# conn.setup_addr(addr) +def server_thread(ctx, sock, addr): + conn = SSL.Connection(ctx, sock) + conn.set_post_connection_check_callback(post_connection_check) + conn.setup_addr(addr) + conn.set_accept_state() + conn.setup_ssl() + conn.accept_ssl() + + post_connection_check(conn) + + print 'SSL Connection opened' + if do_server_loop(conn): + conn.close() + else: + conn.clear() + print 'SSL Connection closed' + + +if __name__=='__main__': + threading.init() + Rand.load_file('../randpool.dat', -1) + + ctx = setup_server_ctx() + + # How about something like this? + #conn_root = SSL.Connection(ctx) + #conn_root.bind(('127.0.0.1', 9999)) + #conn_root.listen(5) + #while 1: + # ssl, addr = conn_root.accept() + # thread.start_new_thread(server_thread, (ctx, ssl, addr)) + + sock = socket(AF_INET, SOCK_STREAM) + sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) + sock.bind(('', 9999)) + sock.listen(5) + while 1: + conn, addr = sock.accept() + thread.start_new_thread(server_thread, (ctx, conn, addr)) + + Rand.save_file('../randpool.dat') + threading.cleanup() diff --git a/demo/ssl/sess.py b/demo/ssl/sess.py new file mode 100644 index 0000000..23428fc --- /dev/null +++ b/demo/ssl/sess.py @@ -0,0 +1,89 @@ +"""M2Crypto.SSL.Session client demo: This program requests a URL from +a HTTPS server, saves the negotiated SSL session id, parses the HTML +returned by the server, then requests each HREF in a separate thread +using the saved SSL session id. + +Copyright (c) 1999-2003 Ng Pheng Siong. All rights reserved.""" + +from M2Crypto import Err, Rand, SSL, X509, threading +m2_threading = threading; del threading + +import formatter, getopt, htmllib, sys +from threading import Thread +from socket import gethostname + + +def handler(sslctx, host, port, href, recurs=0, sslsess=None): + + s = SSL.Connection(sslctx) + if sslsess: + s.set_session(sslsess) + s.connect((host, port)) + else: + s.connect((host, port)) + sslsess = s.get_session() + #print sslsess.as_text() + + if recurs: + p = htmllib.HTMLParser(formatter.NullFormatter()) + + f = s.makefile("rw") + f.write(href) + f.flush() + + while 1: + data = f.read() + if not data: + break + if recurs: + p.feed(data) + + if recurs: + p.close() + + f.close() + + if recurs: + for a in p.anchorlist: + req = 'GET %s HTTP/1.0\r\n\r\n' % a + thr = Thread(target=handler, + args=(sslctx, host, port, req, recurs-1, sslsess)) + print "Thread =", thr.getName() + thr.start() + + +if __name__ == '__main__': + + m2_threading.init() + Rand.load_file('../randpool.dat', -1) + + host = '127.0.0.1' + port = 9443 + req = '/' + + optlist, optarg = getopt.getopt(sys.argv[1:], 'h:p:r:') + for opt in optlist: + if '-h' in opt: + host = opt[1] + elif '-p' in opt: + port = int(opt[1]) + elif '-r' in opt: + req = opt[1] + + ctx = SSL.Context('sslv3') + ctx.load_cert('client.pem') + ctx.load_verify_info('ca.pem') + ctx.load_client_ca('ca.pem') + ctx.set_verify(SSL.verify_none, 10) + + req = 'GET %s HTTP/1.0\r\n\r\n' % req + + start = Thread(target=handler, args=(ctx, host, port, req, 1)) + print "Thread =", start.getName() + start.start() + start.join() + + m2_threading.cleanup() + Rand.save_file('../randpool.dat') + + diff --git a/demo/ssl/sess2.py b/demo/ssl/sess2.py new file mode 100644 index 0000000..a3c2bf4 --- /dev/null +++ b/demo/ssl/sess2.py @@ -0,0 +1,78 @@ +"""M2Crypto.SSL.Session client demo2: This program creates two sockets, each +bound to a different local address. The first creates an SSL connection, the +second then creates another SSL connection using the first's SSL session id. + +(This program only works if you've ifconfig'ed your interfaces correctly, +of course.) + +Copyright (c) 1999-2001 Ng Pheng Siong. All rights reserved.""" + +from M2Crypto import Err, Rand, SSL, X509, threading +m2_threading = threading; del threading + +import formatter, getopt, htmllib, sys +from threading import Thread +from socket import gethostname + +ADDR1 = '127.0.0.1', 9999 +ADDR2 = '127.0.0.2', 9999 + +def handler(addr, sslctx, host, port, req, sslsess=None): + + s = SSL.Connection(sslctx) + s.bind(addr) + if sslsess: + s.set_session(sslsess) + s.connect((host, port)) + else: + s.connect((host, port)) + sslsess = s.get_session() + s.write(req) + while 1: + data = s.read(4096) + if not data: + break + + if addr != ADDR2: + thr = Thread(target=handler, + args=(ADDR2, sslctx, host, port, req, sslsess)) + print "Thread =", thr.getName() + thr.start() + + s.close() + + +if __name__ == '__main__': + + m2_threading.init() + Rand.load_file('../randpool.dat', -1) + + host = '127.0.0.1' + port = 443 + req = '/' + + optlist, optarg = getopt.getopt(sys.argv[1:], 'h:p:r:') + for opt in optlist: + if '-h' in opt: + host = opt[1] + elif '-p' in opt: + port = int(opt[1]) + elif '-r' in opt: + req = opt[1] + + ctx = SSL.Context('sslv3') + ctx.load_cert('client.pem') + ctx.load_verify_info('ca.pem') + ctx.set_verify(SSL.verify_none, 10) + + req = 'GET %s HTTP/1.0\r\n\r\n' % req + + start = Thread(target=handler, args=(ADDR1, ctx, host, port, req)) + print "Thread =", start.getName() + start.start() + start.join() + + m2_threading.cleanup() + Rand.save_file('../randpool.dat') + + diff --git a/demo/ssl/sess2.ssldump.out b/demo/ssl/sess2.ssldump.out new file mode 100644 index 0000000..9b383ee --- /dev/null +++ b/demo/ssl/sess2.ssldump.out @@ -0,0 +1,112 @@ +New TCP connection #1: localhost(9999) <-> localhost(443) +1 1 0.0061 (0.0061) C>S Handshake + ClientHello + Version 3.0 + cipher suites + SSL_DHE_RSA_WITH_3DES_EDE_CBC_SHA + SSL_DHE_DSS_WITH_3DES_EDE_CBC_SHA + SSL_RSA_WITH_3DES_EDE_CBC_SHA + SSL_DHE_DSS_WITH_RC4_128_SHA + SSL_RSA_WITH_IDEA_CBC_SHA + SSL_RSA_WITH_RC4_128_SHA + SSL_RSA_WITH_RC4_128_MD5 + SSL_DHE_DSS_WITH_RC2_56_CBC_SHA + SSL_RSA_EXPORT1024_WITH_RC4_56_SHA + SSL_DHE_DSS_EXPORT1024_WITH_DES_CBC_SHA + SSL_RSA_EXPORT1024_WITH_DES_CBC_SHA + SSL_RSA_EXPORT1024_WITH_RC2_CBC_56_MD5 + SSL_RSA_EXPORT1024_WITH_RC4_56_MD5 + SSL_DHE_RSA_WITH_DES_CBC_SHA + SSL_DHE_DSS_WITH_DES_CBC_SHA + SSL_RSA_WITH_DES_CBC_SHA + SSL_DHE_RSA_EXPORT_WITH_DES40_CBC_SHA + SSL_DHE_DSS_EXPORT_WITH_DES40_CBC_SHA + SSL_RSA_EXPORT_WITH_DES40_CBC_SHA + SSL_RSA_EXPORT_WITH_RC2_CBC_40_MD5 + SSL_RSA_EXPORT_WITH_RC4_40_MD5 + compression methods + NULL +1 2 0.0068 (0.0006) S>C Handshake + ServerHello + Version 3.0 + session_id[32]= + f2 6e ab c1 e6 db fa 55 4d 77 97 be 0d 28 23 fe + 53 8a d5 3b 31 58 1d 93 35 65 10 a9 06 6b a2 a6 + cipherSuite SSL_DHE_RSA_WITH_3DES_EDE_CBC_SHA + compressionMethod NULL +1 3 0.0846 (0.0778) S>C Handshake + Certificate +1 4 0.0848 (0.0001) S>C Handshake + ServerKeyExchange +1 5 0.0848 (0.0000) S>C Handshake + ServerHelloDone +1 6 0.2475 (0.1627) C>S Handshake + ClientKeyExchange + DiffieHellmanClientPublicValue[128]= + 3c 8f 0f 93 8a 3a 1e 5c 07 cf 40 2f 7d bf 25 7a + e8 2b ba 54 46 d5 54 40 22 90 38 f2 88 c3 62 8a + ec b1 b7 f1 06 6d fa ba 46 dd f1 92 5f 44 18 44 + 8e df 33 30 64 79 e0 77 07 1f bc 10 dc 0d 6e 4b + 4d 68 01 af 4a bf 83 62 de 87 d7 98 6c e3 9b af + a2 a6 60 67 18 46 89 29 fa 1a 72 df 92 2d 9e 4f + 2c b2 02 b3 ef b7 03 07 49 69 c5 b2 37 ae a1 0e + 10 e1 79 25 a4 70 02 15 69 d9 47 2c c8 48 23 67 +1 7 0.2475 (0.0000) C>S ChangeCipherSpec +1 8 0.2475 (0.0000) C>S Handshake +1 9 0.3239 (0.0763) S>C ChangeCipherSpec +1 10 0.3239 (0.0000) S>C Handshake +1 11 0.3266 (0.0027) C>S application_data +1 12 0.3566 (0.0299) S>C application_data +1 13 0.3571 (0.0005) S>C Alert +1 0.3574 (0.0003) S>C TCP FIN +New TCP connection #2: 127.0.0.2(9999) <-> localhost(443) +2 1 0.0039 (0.0039) C>S Handshake + ClientHello + Version 3.0 + resume [32]= + f2 6e ab c1 e6 db fa 55 4d 77 97 be 0d 28 23 fe + 53 8a d5 3b 31 58 1d 93 35 65 10 a9 06 6b a2 a6 + cipher suites + SSL_DHE_RSA_WITH_3DES_EDE_CBC_SHA + SSL_DHE_DSS_WITH_3DES_EDE_CBC_SHA + SSL_RSA_WITH_3DES_EDE_CBC_SHA + SSL_DHE_DSS_WITH_RC4_128_SHA + SSL_RSA_WITH_IDEA_CBC_SHA + SSL_RSA_WITH_RC4_128_SHA + SSL_RSA_WITH_RC4_128_MD5 + SSL_DHE_DSS_WITH_RC2_56_CBC_SHA + SSL_RSA_EXPORT1024_WITH_RC4_56_SHA + SSL_DHE_DSS_EXPORT1024_WITH_DES_CBC_SHA + SSL_RSA_EXPORT1024_WITH_DES_CBC_SHA + SSL_RSA_EXPORT1024_WITH_RC2_CBC_56_MD5 + SSL_RSA_EXPORT1024_WITH_RC4_56_MD5 + SSL_DHE_RSA_WITH_DES_CBC_SHA + SSL_DHE_DSS_WITH_DES_CBC_SHA + SSL_RSA_WITH_DES_CBC_SHA + SSL_DHE_RSA_EXPORT_WITH_DES40_CBC_SHA + SSL_DHE_DSS_EXPORT_WITH_DES40_CBC_SHA + SSL_RSA_EXPORT_WITH_DES40_CBC_SHA + SSL_RSA_EXPORT_WITH_RC2_CBC_40_MD5 + SSL_RSA_EXPORT_WITH_RC4_40_MD5 + compression methods + NULL +2 2 0.0055 (0.0016) S>C Handshake + ServerHello + Version 3.0 + session_id[32]= + f2 6e ab c1 e6 db fa 55 4d 77 97 be 0d 28 23 fe + 53 8a d5 3b 31 58 1d 93 35 65 10 a9 06 6b a2 a6 + cipherSuite SSL_DHE_RSA_WITH_3DES_EDE_CBC_SHA + compressionMethod NULL +2 3 0.0055 (0.0000) S>C ChangeCipherSpec +2 4 0.0055 (0.0000) S>C Handshake +2 5 0.0066 (0.0010) C>S ChangeCipherSpec +2 6 0.0066 (0.0000) C>S Handshake +1 14 0.3698 (0.0124) C>S Alert +1 0.3702 (0.0004) C>S TCP FIN +2 7 0.0996 (0.0930) C>S application_data +2 8 0.1224 (0.0227) S>C application_data +2 9 0.1244 (0.0019) S>C Alert +2 10 0.1248 (0.0004) C>S Alert +2 0.1254 (0.0005) C>S TCP FIN +2 0.1300 (0.0046) S>C TCP FIN diff --git a/demo/ssl/socklib.py b/demo/ssl/socklib.py new file mode 100644 index 0000000..ebaf6d3 --- /dev/null +++ b/demo/ssl/socklib.py @@ -0,0 +1,46 @@ +""" +socklib provides a way to transparently replace socket.ssl with +M2Crypto.SSL.Connection. + +Usage: Import socklib before the 3rd party module that uses socket.ssl. Also, + call socketlib.setSSLContextFactory() to set it up with a way to get + secure SSL contexts. + +Copyright (c) 2007 Open Source Applications Foundation. +All rights reserved. +""" + +sslContextFactory = None + +def setSSLContextFactory(factory): + global sslContextFactory + sslContextFactory = factory + +from M2Crypto.SSL import Connection, Checker +import socket + +class ssl_socket(socket.socket): + def connect(self, addr, *args): + self.addr = addr + return super(ssl_socket, self).connect(addr, *args) + + def close(self): + if hasattr(self, 'conn'): + self.conn.close() + socket.socket.close(self) + +def ssl(sock): + sock.conn = Connection(ctx=sslContextFactory(), sock=sock) + sock.conn.addr = sock.addr + sock.conn.setup_ssl() + sock.conn.set_connect_state() + sock.conn.connect_ssl() + check = getattr(sock.conn, 'postConnectionCheck', sock.conn.clientPostConnectionCheck) + if check is not None: + if not check(sock.conn.get_peer_cert(), sock.conn.addr[0]): + raise Checker.SSLVerificationError, 'post connection check failed' + return sock.conn + +socket.socket = ssl_socket +socket.ssl = ssl + diff --git a/demo/ssl/somelib.py b/demo/ssl/somelib.py new file mode 100644 index 0000000..21b3491 --- /dev/null +++ b/demo/ssl/somelib.py @@ -0,0 +1,21 @@ +""" +Sample 3rd party lib to use with socklib and myapp. + +Copyright (c) 2007 Open Source Applications Foundation. +All rights reserved. +""" +# This represents some 3rd party library we don't want to modify + +import socket + +class HttpsGetSlash(object): + def __init__(self): + self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + + def get(self, host, port): + self.socket.connect((host, port)) + ssl_sock = socket.ssl(self.socket) + ssl_sock.write('GET / HTTP/1.0\n\n') + print ssl_sock.read() + self.socket.close()
\ No newline at end of file diff --git a/demo/ssl/ss.py b/demo/ssl/ss.py new file mode 100644 index 0000000..3db6752 --- /dev/null +++ b/demo/ssl/ss.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python + +import os, popen2, time +from socket import * + +def main0(): + cin, cout = popen2.popen2('openssl s_server') + cout.write('Q\n') + cout.flush() + s = socket(AF_INET, SOCK_STREAM) + s.connect(('', 4433)) + s.close() + +def main(): + pid = os.fork() + if pid: + time.sleep(1) + os.kill(pid, 1) + os.waitpid(pid, 0) + else: + os.execvp('openssl', ('s_server',)) + +if __name__ == '__main__': + main() + diff --git a/demo/ssl/twistedsslclient.py b/demo/ssl/twistedsslclient.py new file mode 100755 index 0000000..e4b79c0 --- /dev/null +++ b/demo/ssl/twistedsslclient.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python +""" +Demonstrates M2Crypto.SSL.TwistedProtocolWrapper + +Copyright (c) 2005 Open Source Applications Foundation. All rights reserved. +""" + +import twisted.internet.protocol as protocol +import twisted.protocols.basic as basic +import twisted.internet.reactor as reactor +import M2Crypto.SSL.TwistedProtocolWrapper as wrapper +import M2Crypto.SSL as SSL + +class EchoClient(basic.LineReceiver): + def connectionMade(self): + self.sendLine('Hello World!') + + def lineReceived(self, line): + print 'received: "%s"' % line + self.transport.loseConnection() + + +class EchoClientFactory(protocol.ClientFactory): + protocol = EchoClient + + def clientConnectionFailed(self, connector, reason): + print 'connection failed' + reactor.stop() + + def clientConnectionLost(self, connector, reason): + print 'connection lost' + reactor.stop() + + +class ContextFactory: + def getContext(self): + return SSL.Context() + + +if __name__ == '__main__': + factory = EchoClientFactory() + wrapper.connectSSL('localhost', 8000, factory, ContextFactory()) + reactor.run() # This will block until reactor.stop() is called diff --git a/demo/ssl/twistedsslserver.py b/demo/ssl/twistedsslserver.py new file mode 100755 index 0000000..04897a0 --- /dev/null +++ b/demo/ssl/twistedsslserver.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python +""" +Demonstrates M2Crypto.SSL.TwistedProtocolWrapper + +Copyright (c) 2005 Open Source Applications Foundation. All rights reserved. +""" + +import sys +import M2Crypto.SSL as SSL +import M2Crypto.SSL.TwistedProtocolWrapper as wrapper +import twisted.internet.protocol as protocol +import twisted.internet.reactor as reactor +import twisted.python.log as log + + +class Echo(protocol.Protocol): + def dataReceived(self, data): + print 'received: "%s"' % data + self.transport.write(data) + + def connectionMade(self): + print 'connection made' + + +class ContextFactory: + def getContext(self): + ctx = SSL.Context() + ctx.load_cert('server.pem') + return ctx + + +if __name__ == '__main__': + log.startLogging(sys.stdout) + factory = protocol.Factory() + factory.protocol = Echo + wrapper.listenSSL(8000, factory, ContextFactory()) + reactor.run() diff --git a/demo/ssl/xmlrpc_cli.py b/demo/ssl/xmlrpc_cli.py new file mode 100644 index 0000000..69647d7 --- /dev/null +++ b/demo/ssl/xmlrpc_cli.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python + +"""Demonstration of M2Crypto.xmlrpclib2. + +Copyright (c) 1999-2004 Ng Pheng Siong. All rights reserved.""" + +from M2Crypto import Rand +from M2Crypto.m2xmlrpclib import Server, SSL_Transport + +def ZServerSSL(): + # Server is Zope-2.6.4 on ZServerSSL/0.12. + zs = Server('https://127.0.0.1:8443/', SSL_Transport()) + print zs.propertyMap() + +def xmlrpc_srv(): + # Server is ../https/START_xmlrpc.py or ./xmlrpc_srv.py. + zs = Server('https://127.0.0.1:39443', SSL_Transport()) + print zs.Testing(1, 2, 3) + print zs.BringOn('SOAP') + +if __name__ == '__main__': + Rand.load_file('../randpool.dat', -1) + #ZServerSSL() + xmlrpc_srv() + Rand.save_file('../randpool.dat') + diff --git a/demo/ssl/xmlrpc_srv.py b/demo/ssl/xmlrpc_srv.py new file mode 100644 index 0000000..3811992 --- /dev/null +++ b/demo/ssl/xmlrpc_srv.py @@ -0,0 +1,26 @@ +"""Server demonstration of M2Crypto.xmlrpclib2. + +Copyright (c) 1999-2003 Ng Pheng Siong. All rights reserved.""" + +# M2Crypto +from M2Crypto import DH, SSL +from echod_lib import init_context + +# /F's xmlrpcserver.py. +from xmlrpcserver import RequestHandler + +class xmlrpc_handler(RequestHandler): + def call(self, method, params): + print "XMLRPC call:", method, params + return params + + def finish(self): + self.request.set_shutdown(SSL.SSL_RECEIVED_SHUTDOWN | SSL.SSL_SENT_SHUTDOWN) + self.request.close() + +if __name__ == '__main__': + ctx = init_context('sslv23', 'server.pem', 'ca.pem', SSL.verify_none) + ctx.set_tmp_dh('dh1024.pem') + s = SSL.ThreadingSSLServer(('', 9443), xmlrpc_handler, ctx) + s.serve_forever() + diff --git a/demo/tinderbox/build_lib.py b/demo/tinderbox/build_lib.py new file mode 100644 index 0000000..2babb14 --- /dev/null +++ b/demo/tinderbox/build_lib.py @@ -0,0 +1,162 @@ +# Copyright (c) 2006-2007 Open Source Applications Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Trimmed down for M2Crypto build purposes + +import os, sys +import glob +import fnmatch +import shutil +import fileinput +import errno +import subprocess +import killableprocess +import tempfile + + +_logFilename = 'tbox.log' +_logPrefix = '' +_logFile = None +_logEcho = True +_logEchoErrors = False + + +def initLog(filename, prefix='', echo=True, echoErrors=False): + """ + Initialize log file and store log parameters + + Note: initLog assumes it is called only once per program + """ + global _logFilename, _logPrefix, _logFile, _logEcho, _logEchoErrors + + _logFilename = filename or _logFilename + _logEcho = echo + _logEchoErrors = echoErrors + _logPrefix = prefix + + try: + _logFile = open(_logFilename, 'w+') + result = True + except: + result = False + + return result + + +def closeLog(): + """Need to close log to flush all data.""" + _logFile.close() + + +def log(msg, error=False, newline='\n'): + """ + Output log message to an open log file or to StdOut + """ + echo = _logEcho + + if _logFile is None: + if error or _logEcho: + echo = True + else: + _logFile.write('%s%s%s' % (_logPrefix, msg, newline)) + + if error and _logEchoErrors: + sys.stderr.write('%s%s%s' % (_logPrefix, msg, newline)) + + if echo: + sys.stdout.write('%s%s%s' % (_logPrefix, msg, newline)) + sys.stdout.flush() + + +def setpgid_preexec_fn(): + os.setpgid(0, 0) + + +def runCommand(cmd, env=None, timeout=-1, logger=log, ignorepreexec=False): + """ + Execute the given command and log all output + + Success and failure codes: + + >>> runCommand(['true']) + 0 + >>> runCommand(['false']) + 1 + + Interleaved stdout and stderr messages: + + >>> runCommand(['python', '-c', r'print 1;import sys;sys.stdout.flush();print >>sys.stderr, 2;print 3']) + 1 + 2 + 3 + 0 + + Now with timeout: + + >>> runCommand(['python', '-c', r'print 1;import sys;sys.stdout.flush();print >>sys.stderr, 2;print 3'], timeout=5) + 1 + 2 + 3 + 0 + + Setting environment variable: + + >>> runCommand(['python', '-c', 'import os;print os.getenv("ENVTEST")'], env={'ENVTEST': '42'}) + 42 + 0 + + Timeout: + >>> runCommand(['sleep', '60'], timeout=5) + -9 + """ + redirect = True + + if logger == log and _logFile is None: + redirect = False + else: + if timeout == -1: + output = subprocess.PIPE + else: + output = tempfile.TemporaryFile() + + if ignorepreexec: + preexec_fn = None + else: + preexec_fn = setpgid_preexec_fn + + if redirect: + p = killableprocess.Popen(cmd, env=env, stdin=subprocess.PIPE, stdout=output, stderr=subprocess.STDOUT, preexec_fn=preexec_fn) + else: + p = killableprocess.Popen(cmd, env=env, stdin=subprocess.PIPE, preexec_fn=preexec_fn) + + try: + if timeout == -1 and redirect: + for line in p.stdout: + logger(line[:-1]) + + p.wait(timeout=timeout, group=True) + + except KeyboardInterrupt: + try: + p.kill(group=True) + + except OSError: + p.wait(30) + + if timeout != -1 and redirect: + output.seek(0) + for line in output: + logger(line[:-1]) + + return p.returncode diff --git a/demo/tinderbox/killableprocess.py b/demo/tinderbox/killableprocess.py new file mode 100644 index 0000000..9c4fdb0 --- /dev/null +++ b/demo/tinderbox/killableprocess.py @@ -0,0 +1,212 @@ +# killableprocess - subprocesses which can be reliably killed +# +# Parts of this module are copied from the subprocess.py file contained +# in the Python distribution. +# +# Copyright (c) 2003-2004 by Peter Astrand <astrand@lysator.liu.se> +# +# Additions and modifications written by Benjamin Smedberg +# <benjamin@smedbergs.us> are Copyright (c) 2006 by the Mozilla Foundation +# <http://www.mozilla.org/> +# +# By obtaining, using, and/or copying this software and/or its +# associated documentation, you agree that you have read, understood, +# and will comply with the following terms and conditions: +# +# Permission to use, copy, modify, and distribute this software and +# its associated documentation for any purpose and without fee is +# hereby granted, provided that the above copyright notice appears in +# all copies, and that both that copyright notice and this permission +# notice appear in supporting documentation, and that the name of the +# author not be used in advertising or publicity pertaining to +# distribution of the software without specific, written prior +# permission. +# +# THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, +# INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. +# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, INDIRECT OR +# CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +# OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, +# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION +# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +r"""killableprocess - Subprocesses which can be reliably killed + +This module is a subclass of the builtin "subprocess" module. It allows +processes that launch subprocesses to be reliably killed on Windows (via the Popen.kill() method. + +It also adds a timeout argument to Wait() for a limited period of time before +forcefully killing the process. + +Note: On Windows, this module requires Windows 2000 or higher (no support for +Windows 95, 98, or NT 4.0). It also requires ctypes, which is bundled with +Python 2.5+ or available from http://python.net/crew/theller/ctypes/ +""" + +import subprocess +import sys +import os +import time +import types + +try: + from subprocess import CalledProcessError +except ImportError: + # Python 2.4 doesn't implement CalledProcessError + class CalledProcessError(Exception): + """This exception is raised when a process run by check_call() returns + a non-zero exit status. The exit status will be stored in the + returncode attribute.""" + def __init__(self, returncode, cmd): + self.returncode = returncode + self.cmd = cmd + def __str__(self): + return "Command '%s' returned non-zero exit status %d" % (self.cmd, self.returncode) + +mswindows = (sys.platform == "win32") + +if mswindows: + import winprocess +else: + import signal + +def call(*args, **kwargs): + waitargs = {} + if "timeout" in kwargs: + waitargs["timeout"] = kwargs.pop("timeout") + + return Popen(*args, **kwargs).wait(**waitargs) + +def check_call(*args, **kwargs): + """Call a program with an optional timeout. If the program has a non-zero + exit status, raises a CalledProcessError.""" + + retcode = call(*args, **kwargs) + if retcode: + cmd = kwargs.get("args") + if cmd is None: + cmd = args[0] + raise CalledProcessError(retcode, cmd) + +if not mswindows: + def DoNothing(*args): + pass + +class Popen(subprocess.Popen): + if mswindows: + def _execute_child(self, args, executable, preexec_fn, close_fds, + cwd, env, universal_newlines, startupinfo, + creationflags, shell, + p2cread, p2cwrite, + c2pread, c2pwrite, + errread, errwrite): + if not isinstance(args, types.StringTypes): + args = subprocess.list2cmdline(args) + + if startupinfo is None: + startupinfo = winprocess.STARTUPINFO() + + if None not in (p2cread, c2pwrite, errwrite): + startupinfo.dwFlags |= winprocess.STARTF_USESTDHANDLES + + startupinfo.hStdInput = int(p2cread) + startupinfo.hStdOutput = int(c2pwrite) + startupinfo.hStdError = int(errwrite) + if shell: + startupinfo.dwFlags |= winprocess.STARTF_USESHOWWINDOW + startupinfo.wShowWindow = winprocess.SW_HIDE + comspec = os.environ.get("COMSPEC", "cmd.exe") + args = comspec + " /c " + args + + # We create a new job for this process, so that we can kill + # the process and any sub-processes + self._job = winprocess.CreateJobObject() + + creationflags |= winprocess.CREATE_SUSPENDED + creationflags |= winprocess.CREATE_UNICODE_ENVIRONMENT + + hp, ht, pid, tid = winprocess.CreateProcess( + executable, args, + None, None, # No special security + 1, # Must inherit handles! + creationflags, + winprocess.EnvironmentBlock(env), + cwd, startupinfo) + + self._child_created = True + self._handle = hp + self._thread = ht + self.pid = pid + + winprocess.AssignProcessToJobObject(self._job, hp) + winprocess.ResumeThread(ht) + + if p2cread is not None: + p2cread.Close() + if c2pwrite is not None: + c2pwrite.Close() + if errwrite is not None: + errwrite.Close() + + def kill(self, group=True): + """Kill the process. If group=True, all sub-processes will also be killed.""" + if mswindows: + if group: + winprocess.TerminateJobObject(self._job, 127) + else: + winprocess.TerminateProcess(self._handle, 127) + self.returncode = 127 + else: + if sys.platform == 'cygwin': + cmd = "taskkill /f /pid " + str(self.pid) + if group: + cmd += " /t" + os.system(cmd) + elif group: + os.killpg(self.pid, signal.SIGKILL) + else: + os.kill(self.pid, signal.SIGKILL) + self.returncode = -9 + + def wait(self, timeout=-1, group=True): + """Wait for the process to terminate. Returns returncode attribute. + If timeout seconds are reached and the process has not terminated, + it will be forcefully killed. If timeout is -1, wait will not + time out.""" + + if self.returncode is not None: + return self.returncode + + if mswindows: + if timeout != -1: + timeout = timeout * 1000 + rc = winprocess.WaitForSingleObject(self._handle, timeout) + if rc == winprocess.WAIT_TIMEOUT: + self.kill(group) + else: + self.returncode = winprocess.GetExitCodeProcess(self._handle) + else: + if timeout == -1: + subprocess.Popen.wait(self) + return self.returncode + + starttime = time.time() + + # Make sure there is a signal handler for SIGCHLD installed + oldsignal = signal.signal(signal.SIGCHLD, DoNothing) + + while time.time() < starttime + timeout - 0.01: + pid, sts = os.waitpid(self.pid, os.WNOHANG) + if pid != 0: + self._handle_exitstatus(sts) + signal.signal(signal.SIGCHLD, oldsignal) + return self.returncode + + # time.sleep is interrupted by signals (good!) + newtimeout = timeout - time.time() + starttime + time.sleep(newtimeout) + self.kill(group) + signal.signal(signal.SIGCHLD, oldsignal) + subprocess.Popen.wait(self) + + return self.returncode diff --git a/demo/tinderbox/slave.py b/demo/tinderbox/slave.py new file mode 100755 index 0000000..eaa64c0 --- /dev/null +++ b/demo/tinderbox/slave.py @@ -0,0 +1,172 @@ +#!/usr/bin/env python +# +""" +This is a sample Tinderbox2 buildslave script. + +NOTE: WAIT at least 6 minutes after the last build before starting + the next build! + +Create config.ini file with the following contents: + +[build] +name = identify your build slave, for example Ubuntu 8.04 32-bit +;;optional fields: +;;uname = uname -a +;;swig = swig -version +;;cc = gcc --version +;;openssl = openssl version +;;python = python --version +;;clean = rm -fr m2crypto +;;svn = svn co http://svn.osafoundation.org/m2crypto/trunk m2crypto +;;patch = +;;build = python setup.py clean --all build +;; OR another way to do tests without setuptools: +;;build = PYTHONPATH=build/lib-something python tests/alltests.py +;;test = python setup.py test +;;wait = 3600 +;;timeout = 180 + +[email] +from = your email +to = Email Heikki Toivonen to get the address +user = smtp username +password = smtp password +server = smtp server +port = smtp port +""" + +import time, smtplib, os, ConfigParser, tempfile +import build_lib as bl + +# Change to True when you are troubleshooting this build script +debug_script = False + +# These commands assume we are running on a unix-like system where default +# build options work and all prerequisites are installed and in PATH etc. +DEFAULT_COMMANDS = { + 'uname': ['uname', '-a'], + 'swig': ['swig', '-version'], + 'cc': ['gcc', '--version'], + 'openssl': ['openssl', 'version'], + 'python': ['python', '--version'], + 'clean': ['rm', '-rf', 'm2crypto'], + 'svn': ['svn', 'co', 'http://svn.osafoundation.org/m2crypto/trunk', 'm2crypto'], + 'patch': [], + 'build': ['python', 'setup.py', 'clean', '--all', 'build'], + 'test': ['python', 'setup.py', 'test'] +} + +def load_config(cfg='config.ini'): + config = {} + cp = ConfigParser.ConfigParser() + cp.read(cfg) + for section in cp.sections(): + for option in cp.options(section): + config[option] = cp.get(section, option).strip() + return config + +# XXX copied from test_ssl +def zap_servers(): + s = 's_server' + fn = tempfile.mktemp() + cmd = 'ps | egrep %s > %s' % (s, fn) + os.system(cmd) + f = open(fn) + while 1: + ps = f.readline() + if not ps: + break + chunk = string.split(ps) + pid, cmd = chunk[0], chunk[4] + if cmd == s: + os.kill(int(pid), 1) + f.close() + os.unlink(fn) + +def build(commands, config): + status = 'success' + + cwd = os.getcwd() + timeout = int(config.get('timeout') or 180) + + bl.initLog('tbox.log', echo=debug_script) + + starttime = int(time.time()) + + for command in commands: + cmd = config.get(command) + if not cmd: + cmd = DEFAULT_COMMANDS[command] + if not cmd: + continue + else: + cmd = cmd.split() + + bl.log('*** %s, timeout=%ds' % (' '.join(cmd), timeout)) + + exit_code = bl.runCommand(cmd, timeout=timeout) + if exit_code: + bl.log('*** error exit code = %d' % exit_code) + if command == 'test': + status = 'test_failed' + if os.name != 'nt': + try: + # If tests were killed due to timeout, we may have left + # openssl processes running, so try killing + zap_servers() + except Exception, e: + bl.log('*** error: tried to zap_servers: ' + str(e)) + else: + status = 'build_failed' + break + if command == 'svn': + os.chdir('m2crypto') + + timenow = int(time.time()) + + bl.closeLog() + + os.chdir(cwd) + + return 'tbox.log', starttime, timenow, status + + +def email(logpath, starttime, timenow, status, config): + msg = """From: %(from)s +To: %(to)s +Subject: tree: M2Crypto + + +tinderbox: tree: M2Crypto +tinderbox: starttime: %(starttime)d +tinderbox: timenow: %(timenow)d +tinderbox: status: %(status)s +tinderbox: buildname: %(buildname)s +tinderbox: errorparser: unix +tinderbox: END + +""" % {'from': config['from'], 'to': config['to'], + 'starttime': starttime, 'timenow': timenow, + 'status': status, + 'buildname': config['name']} + + msg += open(logpath).read() + + server = smtplib.SMTP(host=config['server'], port=int(config['port'])) + if debug_script: + server.set_debuglevel(1) + server.starttls() # if your server supports STARTTLS + if config.get('user'): + server.login(config['user'], config['password']) + server.sendmail(config['from'], config['to'], msg) + server.quit() + + +if __name__ == '__main__': + config = load_config() + + commands = ['uname', 'swig', 'cc', 'openssl', 'python', 'clean', 'svn', + 'patch', 'build', 'test'] + + logpath, starttime, timenow, status = build(commands, config) + email(logpath, starttime, timenow, status, config) diff --git a/demo/tinderbox/winprocess.py b/demo/tinderbox/winprocess.py new file mode 100644 index 0000000..4d867fe --- /dev/null +++ b/demo/tinderbox/winprocess.py @@ -0,0 +1,262 @@ +# A module to expose various thread/process/job related structures and +# methods from kernel32 +# +# The MIT License +# +# Copyright (c) 2006 the Mozilla Foundation <http://www.mozilla.org> +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the "Software"), +# to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, +# and/or sell copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. + +from ctypes import c_void_p, POINTER, sizeof, Structure, windll, WinError, WINFUNCTYPE +from ctypes.wintypes import BOOL, BYTE, DWORD, HANDLE, LPCWSTR, LPWSTR, UINT, WORD + +LPVOID = c_void_p +LPBYTE = POINTER(BYTE) +LPDWORD = POINTER(DWORD) + +def ErrCheckBool(result, func, args): + """errcheck function for Windows functions that return a BOOL True + on success""" + if not result: + raise WinError() + return args + +# CloseHandle() + +CloseHandleProto = WINFUNCTYPE(BOOL, HANDLE) +CloseHandle = CloseHandleProto(("CloseHandle", windll.kernel32)) +CloseHandle.errcheck = ErrCheckBool + +# AutoHANDLE + +class AutoHANDLE(HANDLE): + """Subclass of HANDLE which will call CloseHandle() on deletion.""" + def Close(self): + if self.value: + CloseHandle(self) + self.value = 0 + + def __del__(self): + self.Close() + + def __int__(self): + return self.value + +def ErrCheckHandle(result, func, args): + """errcheck function for Windows functions that return a HANDLE.""" + if not result: + raise WinError() + return AutoHANDLE(result) + +# PROCESS_INFORMATION structure + +class PROCESS_INFORMATION(Structure): + _fields_ = [("hProcess", HANDLE), + ("hThread", HANDLE), + ("dwProcessID", DWORD), + ("dwThreadID", DWORD)] + + def __init__(self): + Structure.__init__(self) + + self.cb = sizeof(self) + +LPPROCESS_INFORMATION = POINTER(PROCESS_INFORMATION) + +# STARTUPINFO structure + +class STARTUPINFO(Structure): + _fields_ = [("cb", DWORD), + ("lpReserved", LPWSTR), + ("lpDesktop", LPWSTR), + ("lpTitle", LPWSTR), + ("dwX", DWORD), + ("dwY", DWORD), + ("dwXSize", DWORD), + ("dwYSize", DWORD), + ("dwXCountChars", DWORD), + ("dwYCountChars", DWORD), + ("dwFillAttribute", DWORD), + ("dwFlags", DWORD), + ("wShowWindow", WORD), + ("cbReserved2", WORD), + ("lpReserved2", LPBYTE), + ("hStdInput", HANDLE), + ("hStdOutput", HANDLE), + ("hStdError", HANDLE) + ] +LPSTARTUPINFO = POINTER(STARTUPINFO) + +STARTF_USESHOWWINDOW = 0x01 +STARTF_USESIZE = 0x02 +STARTF_USEPOSITION = 0x04 +STARTF_USECOUNTCHARS = 0x08 +STARTF_USEFILLATTRIBUTE = 0x10 +STARTF_RUNFULLSCREEN = 0x20 +STARTF_FORCEONFEEDBACK = 0x40 +STARTF_FORCEOFFFEEDBACK = 0x80 +STARTF_USESTDHANDLES = 0x100 + +# EnvironmentBlock + +class EnvironmentBlock: + """An object which can be passed as the lpEnv parameter of CreateProcess. + It is initialized with a dictionary.""" + + def __init__(self, dict): + if not dict: + self._as_parameter_ = None + else: + values = ["%s=%s" % (key, value) + for (key, value) in dict.iteritems()] + values.append("") + self._as_parameter_ = LPCWSTR("\0".join(values)) + +# CreateProcess() + +CreateProcessProto = WINFUNCTYPE(BOOL, # Return type + LPCWSTR, # lpApplicationName + LPWSTR, # lpCommandLine + LPVOID, # lpProcessAttributes + LPVOID, # lpThreadAttributes + BOOL, # bInheritHandles + DWORD, # dwCreationFlags + LPVOID, # lpEnvironment + LPCWSTR, # lpCurrentDirectory + LPSTARTUPINFO, # lpStartupInfo + LPPROCESS_INFORMATION # lpProcessInformation + ) + +CreateProcessFlags = ((1, "lpApplicationName", None), + (1, "lpCommandLine"), + (1, "lpProcessAttributes", None), + (1, "lpThreadAttributes", None), + (1, "bInheritHandles", True), + (1, "dwCreationFlags", 0), + (1, "lpEnvironment", None), + (1, "lpCurrentDirectory", None), + (1, "lpStartupInfo"), + (2, "lpProcessInformation")) + +def ErrCheckCreateProcess(result, func, args): + ErrCheckBool(result, func, args) + # return a tuple (hProcess, hThread, dwProcessID, dwThreadID) + pi = args[9] + return AutoHANDLE(pi.hProcess), AutoHANDLE(pi.hThread), pi.dwProcessID, pi.dwThreadID + +CreateProcess = CreateProcessProto(("CreateProcessW", windll.kernel32), + CreateProcessFlags) +CreateProcess.errcheck = ErrCheckCreateProcess + +CREATE_BREAKAWAY_FROM_JOB = 0x01000000 +CREATE_DEFAULT_ERROR_MODE = 0x04000000 +CREATE_NEW_CONSOLE = 0x00000010 +CREATE_NEW_PROCESS_GROUP = 0x00000200 +CREATE_NO_WINDOW = 0x08000000 +CREATE_SUSPENDED = 0x00000004 +CREATE_UNICODE_ENVIRONMENT = 0x00000400 +DEBUG_ONLY_THIS_PROCESS = 0x00000002 +DEBUG_PROCESS = 0x00000001 +DETACHED_PROCESS = 0x00000008 + +# CreateJobObject() + +CreateJobObjectProto = WINFUNCTYPE(HANDLE, # Return type + LPVOID, # lpJobAttributes + LPCWSTR # lpName + ) + +CreateJobObjectFlags = ((1, "lpJobAttributes", None), + (1, "lpName", None)) + +CreateJobObject = CreateJobObjectProto(("CreateJobObjectW", windll.kernel32), + CreateJobObjectFlags) +CreateJobObject.errcheck = ErrCheckHandle + +# AssignProcessToJobObject() + +AssignProcessToJobObjectProto = WINFUNCTYPE(BOOL, # Return type + HANDLE, # hJob + HANDLE # hProcess + ) +AssignProcessToJobObjectFlags = ((1, "hJob"), + (1, "hProcess")) +AssignProcessToJobObject = AssignProcessToJobObjectProto( + ("AssignProcessToJobObject", windll.kernel32), + AssignProcessToJobObjectFlags) +AssignProcessToJobObject.errcheck = ErrCheckBool + +# ResumeThread() + +def ErrCheckResumeThread(result, func, args): + if result == -1: + raise WinError() + + return args + +ResumeThreadProto = WINFUNCTYPE(DWORD, # Return type + HANDLE # hThread + ) +ResumeThreadFlags = ((1, "hThread"),) +ResumeThread = ResumeThreadProto(("ResumeThread", windll.kernel32), + ResumeThreadFlags) +ResumeThread.errcheck = ErrCheckResumeThread + +# TerminateJobObject() + +TerminateJobObjectProto = WINFUNCTYPE(BOOL, # Return type + HANDLE, # hJob + UINT # uExitCode + ) +TerminateJobObjectFlags = ((1, "hJob"), + (1, "uExitCode", 127)) +TerminateJobObject = TerminateJobObjectProto( + ("TerminateJobObject", windll.kernel32), + TerminateJobObjectFlags) +TerminateJobObject.errcheck = ErrCheckBool + +# WaitForSingleObject() + +WaitForSingleObjectProto = WINFUNCTYPE(DWORD, # Return type + HANDLE, # hHandle + DWORD, # dwMilliseconds + ) +WaitForSingleObjectFlags = ((1, "hHandle"), + (1, "dwMilliseconds", -1)) +WaitForSingleObject = WaitForSingleObjectProto( + ("WaitForSingleObject", windll.kernel32), + WaitForSingleObjectFlags) + +INFINITE = -1 +WAIT_TIMEOUT = 0x0102 +WAIT_OBJECT_0 = 0x0 +WAIT_ABANDONED = 0x0080 + +# GetExitCodeProcess() + +GetExitCodeProcessProto = WINFUNCTYPE(BOOL, # Return type + HANDLE, # hProcess + LPDWORD, # lpExitCode + ) +GetExitCodeProcessFlags = ((1, "hProcess"), + (2, "lpExitCode")) +GetExitCodeProcess = GetExitCodeProcessProto( + ("GetExitCodeProcess", windll.kernel32), + GetExitCodeProcessFlags) +GetExitCodeProcess.errcheck = ErrCheckBool diff --git a/demo/x509/ca.py b/demo/x509/ca.py new file mode 100644 index 0000000..446998b --- /dev/null +++ b/demo/x509/ca.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python + +""" +How to create a CA certificate with Python. + +WARNING: This sample only demonstrates how to use the objects and methods, + not how to create a safe and correct certificate. + +Copyright (c) 2004 Open Source Applications Foundation. +Author: Heikki Toivonen +""" + +from M2Crypto import RSA, X509, EVP, m2, Rand, Err + +# XXX Do I actually need more keys? +# XXX Check return values from functions + +def generateRSAKey(): + return RSA.gen_key(2048, m2.RSA_F4) + +def makePKey(key): + pkey = EVP.PKey() + pkey.assign_rsa(key) + return pkey + +def makeRequest(pkey): + req = X509.Request() + req.set_version(2) + req.set_pubkey(pkey) + name = X509.X509_Name() + name.CN = 'My CA, Inc.' + req.set_subject_name(name) + ext1 = X509.new_extension('subjectAltName', 'DNS:foobar.example.com') + ext2 = X509.new_extension('nsComment', 'Hello there') + extstack = X509.X509_Extension_Stack() + extstack.push(ext1) + extstack.push(ext2) + + assert(extstack[1].get_name() == 'nsComment') + + req.add_extensions(extstack) + req.sign(pkey, 'sha1') + return req + +def makeCert(req, caPkey): + pkey = req.get_pubkey() + #woop = makePKey(generateRSAKey()) + #if not req.verify(woop.pkey): + if not req.verify(pkey): + # XXX What error object should I use? + raise ValueError, 'Error verifying request' + sub = req.get_subject() + # If this were a real certificate request, you would display + # all the relevant data from the request and ask a human operator + # if you were sure. Now we just create the certificate blindly based + # on the request. + cert = X509.X509() + # We know we are making CA cert now... + # Serial defaults to 0. + cert.set_serial_number(1) + cert.set_version(2) + cert.set_subject(sub) + issuer = X509.X509_Name() + issuer.CN = 'The Issuer Monkey' + issuer.O = 'The Organization Otherwise Known as My CA, Inc.' + cert.set_issuer(issuer) + cert.set_pubkey(pkey) + notBefore = m2.x509_get_not_before(cert.x509) + notAfter = m2.x509_get_not_after(cert.x509) + m2.x509_gmtime_adj(notBefore, 0) + days = 30 + m2.x509_gmtime_adj(notAfter, 60*60*24*days) + cert.add_ext( + X509.new_extension('subjectAltName', 'DNS:foobar.example.com')) + ext = X509.new_extension('nsComment', 'M2Crypto generated certificate') + ext.set_critical(0)# Defaults to non-critical, but we can also set it + cert.add_ext(ext) + cert.sign(caPkey, 'sha1') + + assert(cert.get_ext('subjectAltName').get_name() == 'subjectAltName') + assert(cert.get_ext_at(0).get_name() == 'subjectAltName') + assert(cert.get_ext_at(0).get_value() == 'DNS:foobar.example.com') + + return cert + +def ca(): + key = generateRSAKey() + pkey = makePKey(key) + req = makeRequest(pkey) + cert = makeCert(req, pkey) + return (cert, pkey) + +if __name__ == '__main__': + Rand.load_file('../randpool.dat', -1) + rsa = generateRSAKey() + pkey = makePKey(rsa) + req = makeRequest(pkey) + print req.as_text() + cert = makeCert(req, pkey) + print cert.as_text() + print cert.as_pem() + cert.save_pem('my_ca_cert.pem') + rsa.save_key('my_key.pem', 'aes_256_cbc') + Rand.save_file('../randpool.dat') diff --git a/demo/x509/certdata2pem.py b/demo/x509/certdata2pem.py new file mode 100755 index 0000000..9fd6d32 --- /dev/null +++ b/demo/x509/certdata2pem.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python +""" +Small utility to convert the Mozilla-format certificates +(/mozilla/security/nss/lib/ckfw/builtins/certdata.txt in the Mozilla CVS) +into PEM. Got the idea from http://curl.haxx.se/docs/parse-certs.txt. + +Copyright (c) 2007 Open Source Applications Foundation. +""" + +import array +from M2Crypto import X509 + +counter = 0 +value = None +name = None + +out = open('cacert.pem', 'wb') + +for line in open('certdata.txt'): + line = line.strip() + if line.startswith('CKA_LABEL'): + assert value is None + + label_encoding, name, dummy = line.split('"') + label, encoding = label_encoding.split() + + assert encoding == 'UTF8' + + elif line == 'CKA_VALUE MULTILINE_OCTAL': + assert name is not None + + value = array.array('c') + + elif value is not None and line == 'END': + assert name is not None + + print 'Writing ' + name + x509 = X509.load_cert_string(value.tostring(), X509.FORMAT_DER) + if not x509.verify(): + print ' Skipping ' + name + ' since it does not verify' + name = None + value = None + continue + counter += 1 + + out.write(name + '\n' + '=' * len(name) + '\n\n') + out.write('SHA1 Fingerprint=' + x509.get_fingerprint('sha1') + '\n') + out.write(x509.as_text()) + out.write(x509.as_pem()) + out.write('\n') + + name = None + value = None + + elif value is not None: + assert name is not None + + for number in line.split('\\'): + if not number: + continue + + value.append(chr(int(number, 8))) + +print 'Wrote %d certificates' % counter diff --git a/demo/x509/client2.pem b/demo/x509/client2.pem new file mode 100644 index 0000000..166919a --- /dev/null +++ b/demo/x509/client2.pem @@ -0,0 +1,36 @@ +-----BEGIN CERTIFICATE----- +MIIDDjCCAnegAwIBAgIBAzANBgkqhkiG9w0BAQQFADB7MQswCQYDVQQGEwJTRzER +MA8GA1UEChMITTJDcnlwdG8xFDASBgNVBAsTC00yQ3J5cHRvIENBMSQwIgYDVQQD +ExtNMkNyeXB0byBDZXJ0aWZpY2F0ZSBNYXN0ZXIxHTAbBgkqhkiG9w0BCQEWDm5n +cHNAcG9zdDEuY29tMB4XDTAwMTEyMDEzMDMwNVoXDTAyMTEyMDEzMDMwNVowWzEL +MAkGA1UEBhMCU0cxETAPBgNVBAoTCE0yQ3J5cHRvMRowGAYDVQQDExFNMkNyeXB0 +byBDbGllbnQgMjEdMBsGCSqGSIb3DQEJARYObmdwc0Bwb3N0MS5jb20wXDANBgkq +hkiG9w0BAQEFAANLADBIAkEAn9qneRYTKPokme3obiJa2NTz1Z2kcF3NHVh60Qod +/TV/q4olPrZdFR2TDWt63Lgnygcsgf3u9pnhcEGk6IvntwIDAQABo4IBBDCCAQAw +CQYDVR0TBAIwADAsBglghkgBhvhCAQ0EHxYdT3BlblNTTCBHZW5lcmF0ZWQgQ2Vy +dGlmaWNhdGUwHQYDVR0OBBYEFDKWFe6VWMhtRTE3/78+hAnSGxmvMIGlBgNVHSME +gZ0wgZqAFPuHI2nrnDqTFeXFvylRT/7tKDgBoX+kfTB7MQswCQYDVQQGEwJTRzER +MA8GA1UEChMITTJDcnlwdG8xFDASBgNVBAsTC00yQ3J5cHRvIENBMSQwIgYDVQQD +ExtNMkNyeXB0byBDZXJ0aWZpY2F0ZSBNYXN0ZXIxHTAbBgkqhkiG9w0BCQEWDm5n +cHNAcG9zdDEuY29tggEAMA0GCSqGSIb3DQEBBAUAA4GBABpE9xt1Hlq2dQZUXHuX +HI57vc2mlWnhhM0wnNhsNZFwfXRHCZOo/JJBhEIT3Rgyz0ErrbOr1SN96HNDKXOD +z6bh4NxB5DZ9sRPKEBj66zDsWJVMlom+Lkeal+GkVy36vpAyP1r+cTXyc9M2Gw/o +FBMinMHH/BXvF5GJ+UleheZe +-----END CERTIFICATE----- +-----BEGIN RSA PRIVATE KEY----- +MIIBOgIBAAJBAJ/ap3kWEyj6JJnt6G4iWtjU89WdpHBdzR1YetEKHf01f6uKJT62 +XRUdkw1rety4J8oHLIH97vaZ4XBBpOiL57cCAwEAAQJANXfspprUo9MvpPEn2pbR +Lk/kk2IcW510e0laI0uwBj50djfHqvsU5ccuVLrxowngLGrFmM3G4lnMknR2NvH8 +0QIhAMsK0AwStUNM/KyvIMikHHBOE9PrK7ARgKvlKl+0ieWPAiEAyYwonIVAtr1f +M8vmrc6TM2YxzSq4+jyYktaaNhYw11kCIA5pmhMBUPSSBm2LkNwtKgeewzGLw/If +i+6nubZJbnBpAiEAvJQvy4PCsTkvQr+d7zJB+O2920IGId1gxMOXNtQ8jsECIGvn +Uz54oonshmTg+Kj2DxnUKQEzFAmQLbtFslp1m47v +-----END RSA PRIVATE KEY----- +-----BEGIN CERTIFICATE REQUEST----- +MIIBFTCBwAIBADBbMQswCQYDVQQGEwJTRzERMA8GA1UEChMITTJDcnlwdG8xGjAY +BgNVBAMTEU0yQ3J5cHRvIENsaWVudCAyMR0wGwYJKoZIhvcNAQkBFg5uZ3BzQHBv +c3QxLmNvbTBcMA0GCSqGSIb3DQEBAQUAA0sAMEgCQQCf2qd5FhMo+iSZ7ehuIlrY +1PPVnaRwXc0dWHrRCh39NX+riiU+tl0VHZMNa3rcuCfKByyB/e72meFwQaToi+e3 +AgMBAAGgADANBgkqhkiG9w0BAQQFAANBAHI5KXfL6kIRoNjR8G9/uKiPUt4uVBKF +ecGp87M5t2a92Z0KpWOMXSHZ0LLQKqwWzALvWcPPIj6S8F6ENdwpfMk= +-----END CERTIFICATE REQUEST----- diff --git a/demo/x509/demo1.py b/demo/x509/demo1.py new file mode 100644 index 0000000..f0aa282 --- /dev/null +++ b/demo/x509/demo1.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python + +"""X.509 certificate manipulation and such. + +Copyright (c) 1999-2003 Ng Pheng Siong. All rights reserved.""" + +import os + +from M2Crypto import X509 +from M2Crypto.EVP import MessageDigest + +def demo1(): + print 'Test 1: As DER...' + cert1 = X509.load_cert('server.pem') + der1 = cert1.as_der() + dgst1 = MessageDigest('sha1') + dgst1.update(der1) + print 'Using M2Crypto:\n', `dgst1.final()`, '\n' + + cert2 = os.popen('openssl x509 -inform pem -outform der -in server.pem') + der2 = cert2.read() + dgst2 = MessageDigest('sha1') + dgst2.update(der2) + print 'Openssl command line:\n', `dgst2.final()`, '\n' + + +def demo2(): + print 'Test 2: As text...' + cert = X509.load_cert('client2.pem') + print 'version ', cert.get_version() + print 'serial# ', cert.get_serial_number() + print 'not before ', cert.get_not_before() + print 'not after ', cert.get_not_after() + issuer = cert.get_issuer() + #print 'issuer ', issuer + print 'issuer.C ', `issuer.C` + print 'issuer.SP ', `issuer.SP` + print 'issuer.L ', `issuer.L` + print 'issuer.O ', `issuer.O` + print 'issuer.OU ', `issuer.OU` + print 'issuer.CN ', `issuer.CN` + print 'issuer.Email', `issuer.Email` + print 'subject ', cert.get_subject() + #print cert.as_text(), '\n' + +def demo3(): + cert = X509.load_cert('server.pem') + while 1: + x = cert.get_subject() + +if __name__ == "__main__": + #demo1() + demo2() + #demo3() diff --git a/demo/x509/proxy_destroy.py b/demo/x509/proxy_destroy.py new file mode 100644 index 0000000..c79bd2f --- /dev/null +++ b/demo/x509/proxy_destroy.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python +############################################################################ +# Matt Rodriguez, LBNL MKRodriguez@lbl.gov +############################################################################ +""" +Script that destroys a proxy certificate file by overwriting its contents +before the file is removed +""" + +import proxylib +import optparse, os + +USAGEHELP = "proxy_destroy.py file1 file2 Destroys files listed" +JUNK = "LalalAlalaLalalALalalAlalaLalalALalalAlalaLalalALalalAlalaLalalA" + +def scrub_file(filename): + """ + Overwrite the file with junk, before removing it + """ + s = os.stat(filename) + proxy_file = file(filename, "w") + size = s.st_size + while size > 64: + proxy_file.write(JUNK) + size -= 64 + + proxy_file.flush() + proxy_file.close() + os.remove(filename) + + +def main(): + parser = optparse.OptionParser() + parser.set_usage(USAGEHELP) + opts, args = parser.parse_args() + if len(args) is 0: + proxy_file = proxylib.get_proxy_filename() + scrub_file(proxy_file) + + for proxy_file in args: + scrub_file(proxy_file) + +if __name__ == "__main__": main() diff --git a/demo/x509/proxy_info.py b/demo/x509/proxy_info.py new file mode 100644 index 0000000..ce9cdc3 --- /dev/null +++ b/demo/x509/proxy_info.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python +############################################################################ +# Matt Rodriguez, LBNL MKRodriguez@lbl.gov +############################################################################ +""" +script that displays information about a proxy certificate +""" + + +import proxylib +import time, datetime, calendar +import sys, optparse + +FILEHELP = "Location of the proxy." + +def print_info(proxy_cert): + """ + Print information about the proxy cert + """ + cert = proxy_cert.getcert() + print "Subject: ", cert.get_subject().as_text() + print "Issuer: ", cert.get_issuer().as_text() + pubkey = cert.get_pubkey() + size = pubkey.size() + print "Strength: ", size * 8 + after = cert.get_not_after() + after_tuple = time.strptime(str(after),"%b %d %H:%M:%S %Y %Z") + expires = calendar.timegm(after_tuple) + now = datetime.timedelta(seconds=time.time()) + expires = datetime.timedelta(seconds=expires) + td = expires - now + if td.days < 0: + print "Time left: Proxy has expired." + else: + hours = td.seconds / 3600 + hours += td.days * 24 + minutes = (td.seconds % 3600) / 60 + seconds = (td.seconds % 3600) % 60 + print "Time left: %d:%d:%d" % (hours, minutes, seconds) + fraction = round((float(td.seconds) / float(3600 * 24)), 1) + print "Days left: ", str(td.days) + str(fraction)[1:] + + +def main(): + parser = optparse.OptionParser() + parser.add_option("-f", "--file", dest="filename", help=FILEHELP) + (opts, args) = parser.parse_args() + filename = opts.filename + if filename is None: + proxyfile = proxylib.get_proxy_filename() + else: + proxyfile = filename + proxy_cert = proxylib.Proxy() + try: + proxy_cert.read(proxyfile) + except IOError: + print "The file: " + proxyfile + " does not exist." + sys.exit(0) + print_info(proxy_cert) + +if __name__ == "__main__": main() diff --git a/demo/x509/proxy_init.py b/demo/x509/proxy_init.py new file mode 100644 index 0000000..1ca5cbb --- /dev/null +++ b/demo/x509/proxy_init.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python +############################################################################ +# Matt Rodriguez, LBNL MKRodriguez@lbl.gov +############################################################################ +""" +script that generates a proxy certificate +""" + +import proxylib +import optparse +import sys + +OUTHELP = "Location of the new proxy cert." +CERTHELP = "Location of user certificate." +KEYHELP = "Location of the user key." +VALIDHELP = "h:m Proxy certificate is valid for h hours and m minutes." +FULLPROXY = "Creates a limited proxy" +def main(): + parser = optparse.OptionParser() + parser.add_option('-o', '--output', dest='output', help=OUTHELP) + parser.add_option('-c', '--cert' , dest='cert', help=CERTHELP) + parser.add_option('-k', '--key', dest='key', help=KEYHELP) + parser.add_option('-v', '--valid', dest='valid', help=VALIDHELP) + parser.add_option('-l', '--limited', action="store_true", + default=False, dest='limited', help=VALIDHELP) + (opts, args) = parser.parse_args() + kw = {} + kw['cert'] = opts.cert + kw['key'] = opts.key + if opts.valid is None: + valid_tuple = (12, 0) + else: + valid = opts.valid.split(':') + valid_tuple = tuple(map(int, valid)) + kw['valid'] = valid_tuple + kw['full'] = not opts.limited + try: + proxy_factory = proxylib.ProxyFactory(kw) + except IOError: + print "Can't find usercert or userkey. Use the -c or -k arguments" + sys.exit(0) + proxy_factory.generate() + proxy_cert = proxy_factory.getproxy() + if opts.output is None: + proxy_cert.write(proxylib.get_proxy_filename()) + else: + proxy_cert.write(opts.output) + +if __name__ == "__main__": main() diff --git a/demo/x509/proxylib.py b/demo/x509/proxylib.py new file mode 100644 index 0000000..e0f3395 --- /dev/null +++ b/demo/x509/proxylib.py @@ -0,0 +1,327 @@ +############################################################################ +# Matt Rodriguez, LBNL +#Copyright (c) 2003, The Regents of the University of California, +#through Lawrence Berkeley National Laboratory +#(subject to receipt of any required approvals from the U.S. Dept. of Energy). +#All rights reserved. +############################################################################ +""" +API to generated proxy certificates +""" +import os, sys +import struct +import re +import time, calendar, datetime + +import_regex = re.compile(r"\s*libssl.so.0.9.8\s*") +errstr = "You must have the openssl 0.9.8 libraries in your LD_LIBRARY_PATH""" + +try: + from M2Crypto import BIO, X509, RSA, EVP, ASN1 +except ImportError, ex: + if import_regex.match(str(ex)): + print errstr + sys.exit(-1) + else: + raise ex + +MBSTRING_FLAG = 0x1000 +MBSTRING_ASC = MBSTRING_FLAG | 1 +KEY_USAGE_VALUE = "Digital Signature, Key Encipherment, Data Encipherment" +PCI_VALUE_FULL = "critical, language:Inherit all" +PCI_VALUE_LIMITED = "critical, language:1.3.6.1.4.1.3536.1.1.1.9" + +def create_write_file(fname, perm=0600): + """ + Creates a file to write to while avoiding a possible race condition. + This is essential for writing out the proxy file. Need to make sure + there is no pre-existing file. + """ + if os.path.exists(fname): + os.remove(fname) + # Make sure the file doesn't exist. Will throw an exception if + # it does. This would only happen if the code is attacked. + fd = os.open(fname, os.O_CREAT|os.O_EXCL|os.O_WRONLY, perm) + f = os.fdopen(fd, 'w') + return f + + +class ProxyFactoryException(Exception): + """ + Base class for exceptions in the ProxyFactory class + """ + +class Proxy: + """ + class that holds proxy certificate information, + consisting of an issuer cert a user cert and + a key for the user cert + """ + def __init__(self): + self._key = None + self._cert = None + self._issuer = None + + def read(self, proxypath=None): + """ + reads in a proxy certificate information + """ + if proxypath is None: + proxypath = get_proxy_filename() + + proxyfile = open(proxypath) + bio = BIO.File(proxyfile) + self._cert = X509.load_cert_bio(bio) + self._key = RSA.load_key_bio(bio) + self._issuer = X509.load_cert_bio(bio) + + def getcert(self): + """ + Returns a X509 instance + """ + return self._cert + + def getkey(self): + """ + Returns a RSA instance + """ + return self._key + + def getissuer(self): + """ + Returns a X509 instance + """ + return self._issuer + + def setcert(self, cert): + """ + Sets the user cert should be a X509 instance + """ + self._cert = cert + + def setkey(self, key): + """ + Sets the user key should be a RSA instance + """ + self._key = key + + def setissuer(self, issuer): + """ + Sets the issuer cert should be a X509 instance + """ + self._issuer = issuer + + def write(self, proxypath=None): + """ + Writes the proxy information to a file + """ + proxyfile = create_write_file(proxypath) + bio = BIO.File(proxyfile) + bio.write(self._cert.as_pem()) + self._key.save_key_bio(bio, cipher=None) + bio.write(self._issuer.as_pem()) + bio.close() + os.chmod(proxypath, 0600) + + +class ProxyFactory: + """ + Creates proxies + """ + def __init__(self, kw={'cert':None,'key':None,'valid':(12,0),'full':True}): + + self._usercert = get_usercert(kw['cert']) + self._userkey = get_userkey(kw['key']) + self._proxycert = None + self._proxykey = None + self._valid = kw['valid'] + self._full = kw['full'] + + def generate(self): + """ + generates a new proxy like grid-proxy-init + """ + if not self._check_valid(): + raise ProxyFactoryException("The issuer cert is expired") + if self._proxycert is None: + self._proxycert = X509.X509() + key = EVP.PKey() + self._proxykey = RSA.gen_key(512, 65537) + key.assign_rsa(self._proxykey, capture=0) + self._proxycert.set_pubkey(key) + self._proxycert.set_version(2) + self._set_times() + issuer_name = self._usercert.get_subject() + self._proxycert.set_issuer_name(issuer_name) + serial_number = self._make_serial_number(self._proxycert) + self._proxycert.set_serial_number(serial_number) + self._set_subject() + sign_pk = EVP.PKey() + sign_pk.assign_rsa(self._userkey) + self._add_extensions() + self._proxycert.sign(sign_pk, 'md5') + + def set_proxycert(self, proxycert): + """ + This method is useful if you don't + want to pay the costs associated with + generating a new key pair. + """ + self._proxycert = proxycert + + def getproxy(self): + """ + Return a proxy instance + """ + proxy = Proxy() + proxy.setissuer(self._usercert) + proxy.setcert(self._proxycert) + proxy.setkey(self._proxykey) + return proxy + + def _set_subject(self): + """ + Internal method that sets the subject name + """ + subject_name = X509.X509_Name() + serial_number = self._make_serial_number(self._proxycert) + issuer_name = self._usercert.get_subject() + issuer_name_txt = issuer_name.as_text() + seq = issuer_name_txt.split(",") + for entry in seq: + name_component = entry.split("=") + subject_name.add_entry_by_txt(field=name_component[0].strip(), + type=MBSTRING_ASC, + entry=name_component[1],len=-1, + loc=-1, set=0) + + + subject_name.add_entry_by_txt(field="CN", + type=MBSTRING_ASC, + entry=str(serial_number), + len=-1, loc=-1, set=0) + + self._proxycert.set_subject_name(subject_name) + + def _set_times(self): + """ + Internal function that sets the time on the proxy + certificate + """ + not_before = ASN1.ASN1_UTCTIME() + not_after = ASN1.ASN1_UTCTIME() + not_before.set_time(int(time.time())) + offset = (self._valid[0] * 3600) + (self._valid[1] * 60) + not_after.set_time(int(time.time()) + offset ) + self._proxycert.set_not_before(not_before) + self._proxycert.set_not_after(not_after) + + def _make_serial_number(self, cert): + """ + Lifted from the globus code + """ + message_digest = EVP.MessageDigest('sha1') + pubkey = cert.get_pubkey() + der_encoding = pubkey.as_der() + message_digest.update(der_encoding) + digest = message_digest.final() + digest_tuple = struct.unpack('BBBB', digest[:4]) + sub_hash = long(digest_tuple[0] + (digest_tuple[1] + ( digest_tuple[2] + + ( digest_tuple[3] >> 1) * 256 ) * 256) * 256) + return sub_hash + + def _add_extensions(self): + """ + Internal method that adds the extensions to the certificate + """ + key_usage_ext = X509.new_extension("keyUsage", KEY_USAGE_VALUE, 1) + self._proxycert.add_ext(key_usage_ext) + if self._full: + pci_ext = X509.new_extension("proxyCertInfo", + PCI_VALUE_FULL, 1, 0) + else: + pci_ext = X509.new_extension("proxyCertInfo", + PCI_VALUE_LIMITED, 1, 0) + self._proxycert.add_ext(pci_ext) + + def _check_valid(self): + """ + Internal method that ensures the issuer cert has + valid, not_before and not_after fields + """ + before_time = self._usercert.get_not_before() + after_time = self._usercert.get_not_after() + before_tuple = time.strptime(str(before_time), "%b %d %H:%M:%S %Y %Z") + after_tuple = time.strptime(str(after_time), "%b %d %H:%M:%S %Y %Z") + starts = datetime.timedelta(seconds=calendar.timegm(before_tuple)) + expires = datetime.timedelta(seconds=calendar.timegm(after_tuple)) + now = datetime.timedelta(seconds=time.time()) + time_delta = expires - now + #cert has expired + if time_delta.days < 0: + return False + #cert is not yet valid, not likely but should still return False + time_delta = now - starts + if time_delta.days < 0: + return False + + return True + +#Utility Functions +def get_proxy_filename(): + """ + function that returns the default proxy path + which is /tmp/x509up_uuid + """ + if os.name == 'posix': + proxy_filename = "x509up_u" + (str(os.getuid())) + proxypath = os.path.join("/tmp", proxy_filename) + elif os.name == 'nt': + username = os.getenv("USERNAME") + if username is None: + raise RuntimeError("""USERNAME is not set in environment. Can't + determine proxy file location""") + + proxy_filename = "x509up_u" + username + drive = os.path.splitdrive(os.getcwd())[0] + proxydir = drive + os.sep + "temp" + proxypath = os.path.join(proxydir, proxy_filename) + else: + except_string = """get_proxy_filename is not supported on this platform + Try explicitly specifying the location of the + proxyfile""" + raise RuntimeError(except_string) + return proxypath + +def get_usercert(certfile=None): + """ + function that returns a X509 instance which + is the user cert that is expected to be a ~/.globus/usercert.pem + + A check is performed to ensure the certificate has valid + before and after times. + """ + if certfile is None: + certfile = open(os.path.join(os.getenv("HOME"), + ".globus","usercert.pem")) + else: + certfile = open(certfile) + bio = BIO.File(certfile) + cert = X509.load_cert_bio(bio) + return cert + +def get_userkey(keyfile=None): + """ + function that returns a X509 instance which + is the user cert that is expected to be a ~/.globus/userkey.pem + """ + if keyfile is None: + keyfile = open(os.path.join(os.getenv("HOME"), + ".globus","userkey.pem")) + else: + keyfile = open(keyfile) + bio = BIO.File(keyfile) + key = RSA.load_key_bio(bio) + return key + + diff --git a/demo/x509/server-expired.pem b/demo/x509/server-expired.pem new file mode 100644 index 0000000..80ef9dc --- /dev/null +++ b/demo/x509/server-expired.pem @@ -0,0 +1,36 @@ +-----BEGIN CERTIFICATE----- +MIIDBjCCAm+gAwIBAgIBATANBgkqhkiG9w0BAQQFADB7MQswCQYDVQQGEwJTRzER +MA8GA1UEChMITTJDcnlwdG8xFDASBgNVBAsTC00yQ3J5cHRvIENBMSQwIgYDVQQD +ExtNMkNyeXB0byBDZXJ0aWZpY2F0ZSBNYXN0ZXIxHTAbBgkqhkiG9w0BCQEWDm5n +cHNAcG9zdDEuY29tMB4XDTAwMDkxMDA5NTEzMFoXDTAyMDkxMDA5NTEzMFowUzEL +MAkGA1UEBhMCU0cxETAPBgNVBAoTCE0yQ3J5cHRvMRIwEAYDVQQDEwlsb2NhbGhv +c3QxHTAbBgkqhkiG9w0BCQEWDm5ncHNAcG9zdDEuY29tMFwwDQYJKoZIhvcNAQEB +BQADSwAwSAJBAKy+e3dulvXzV7zoTZWc5TzgApr8DmeQHTYC8ydfzH7EECe4R1Xh +5kwIzOuuFfn178FBiS84gngaNcrFi0Z5fAkCAwEAAaOCAQQwggEAMAkGA1UdEwQC +MAAwLAYJYIZIAYb4QgENBB8WHU9wZW5TU0wgR2VuZXJhdGVkIENlcnRpZmljYXRl +MB0GA1UdDgQWBBTPhIKSvnsmYsBVNWjj0m3M2z0qVTCBpQYDVR0jBIGdMIGagBT7 +hyNp65w6kxXlxb8pUU/+7Sg4AaF/pH0wezELMAkGA1UEBhMCU0cxETAPBgNVBAoT +CE0yQ3J5cHRvMRQwEgYDVQQLEwtNMkNyeXB0byBDQTEkMCIGA1UEAxMbTTJDcnlw +dG8gQ2VydGlmaWNhdGUgTWFzdGVyMR0wGwYJKoZIhvcNAQkBFg5uZ3BzQHBvc3Qx +LmNvbYIBADANBgkqhkiG9w0BAQQFAAOBgQA7/CqT6PoHycTdhEStWNZde7M/2Yc6 +BoJuVwnW8YxGO8Sn6UJ4FeffZNcYZddSDKosw8LtPOeWoK3JINjAk5jiPQ2cww++ +7QGG/g5NDjxFZNDJP1dGiLAxPW6JXwov4v0FmdzfLOZ01jDcgQQZqEpYlgpuI5JE +WUQ9Ho4EzbYCOQ== +-----END CERTIFICATE----- +-----BEGIN RSA PRIVATE KEY----- +MIIBPAIBAAJBAKy+e3dulvXzV7zoTZWc5TzgApr8DmeQHTYC8ydfzH7EECe4R1Xh +5kwIzOuuFfn178FBiS84gngaNcrFi0Z5fAkCAwEAAQJBAIqm/bz4NA1H++Vx5Ewx +OcKp3w19QSaZAwlGRtsUxrP7436QjnREM3Bm8ygU11BjkPVmtrKm6AayQfCHqJoT +ZIECIQDW0BoMoL0HOYM/mrTLhaykYAVqgIeJsPjvkEhTFXWBuQIhAM3deFAvWNu4 +nklUQ37XsCT2c9tmNt1LAT+slG2JOTTRAiAuXDtC/m3NYVwyHfFm+zKHRzHkClk2 +HjubeEgjpj32AQIhAJqMGTaZVOwevTXvvHwNEH+vRWsAYU/gbx+OQB+7VOcBAiEA +oolb6NMg/R3enNPvS1O4UU1H8wpaF77L4yiSWlE0p4w= +-----END RSA PRIVATE KEY----- +-----BEGIN CERTIFICATE REQUEST----- +MIIBDTCBuAIBADBTMQswCQYDVQQGEwJTRzERMA8GA1UEChMITTJDcnlwdG8xEjAQ +BgNVBAMTCWxvY2FsaG9zdDEdMBsGCSqGSIb3DQEJARYObmdwc0Bwb3N0MS5jb20w +XDANBgkqhkiG9w0BAQEFAANLADBIAkEArL57d26W9fNXvOhNlZzlPOACmvwOZ5Ad +NgLzJ1/MfsQQJ7hHVeHmTAjM664V+fXvwUGJLziCeBo1ysWLRnl8CQIDAQABoAAw +DQYJKoZIhvcNAQEEBQADQQA7uqbrNTjVWpF6By5ZNPvhZ4YdFgkeXFVWi5ao/TaP +Vq4BG021fJ9nlHRtr4rotpgHDX1rr+iWeHKsx4+5DRSy +-----END CERTIFICATE REQUEST----- diff --git a/demo/x509/server.pem b/demo/x509/server.pem new file mode 100644 index 0000000..1ee9282 --- /dev/null +++ b/demo/x509/server.pem @@ -0,0 +1,36 @@ +-----BEGIN CERTIFICATE----- +MIIDYjCCAsugAwIBAgIBBDANBgkqhkiG9w0BAQQFADCBgDELMAkGA1UEBhMCU0cx +ETAPBgNVBAoTCE0yQ3J5cHRvMRQwEgYDVQQLEwtNMkNyeXB0byBDQTEkMCIGA1UE +AxMbTTJDcnlwdG8gQ2VydGlmaWNhdGUgTWFzdGVyMSIwIAYJKoZIhvcNAQkBFhNu +Z3BzQG5ldG1lbWV0aWMuY29tMB4XDTAzMDYyMjEzMzAxNFoXDTA0MDYyMTEzMzAx +NFowXTELMAkGA1UEBhMCU0cxETAPBgNVBAoTCE0yQ3J5cHRvMRIwEAYDVQQDEwls +b2NhbGhvc3QxJzAlBgkqhkiG9w0BCQEWGGFkbWluQHNlcnZlci5leGFtcGxlLmRv +bTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA37aKGZtFicl8xXXTLJ8/JD7c +kd3t/teCX9i61lpaDQCKBoVrrursIvZihemMKI9g/u/+BLqt5g8mBdgUdYz0txc8 +KEbV2hj+wwOX4H3XwD0Y+DysXiNHq7/tFdmzSVHoLxpY4zYzXbxQ/p049wvIyPRp +/y3omcnx/TEUhkn+JmkCAwEAAaOCAQwwggEIMAkGA1UdEwQCMAAwLAYJYIZIAYb4 +QgENBB8WHU9wZW5TU0wgR2VuZXJhdGVkIENlcnRpZmljYXRlMB0GA1UdDgQWBBTB +H/mUYlww24mJlSxEtGdlwojO9zCBrQYDVR0jBIGlMIGigBTr+pwHMS1CmF9cuGI4 +du3YqoHAwqGBhqSBgzCBgDELMAkGA1UEBhMCU0cxETAPBgNVBAoTCE0yQ3J5cHRv +MRQwEgYDVQQLEwtNMkNyeXB0byBDQTEkMCIGA1UEAxMbTTJDcnlwdG8gQ2VydGlm +aWNhdGUgTWFzdGVyMSIwIAYJKoZIhvcNAQkBFhNuZ3BzQG5ldG1lbWV0aWMuY29t +ggEAMA0GCSqGSIb3DQEBBAUAA4GBAAvl6v0s3eFeGP4iAcrfysuK7jzFKhjDYuOy +lVS3u33bZNLnMpM6OSEM9yPh4WpFCVHf+nYwC71pk4ilsLVXjKxymm2lNGcxLVuW +iydFz4Ly9nmN7Ja9ygYT39dGAFP/wN7ELTpsbul8VfmqhNg9y81d8i/A1tK3AGA8 +0QkPQNdP +-----END CERTIFICATE----- +-----BEGIN RSA PRIVATE KEY----- +MIICXAIBAAKBgQDftooZm0WJyXzFddMsnz8kPtyR3e3+14Jf2LrWWloNAIoGhWuu +6uwi9mKF6Ywoj2D+7/4Euq3mDyYF2BR1jPS3FzwoRtXaGP7DA5fgfdfAPRj4PKxe +I0erv+0V2bNJUegvGljjNjNdvFD+nTj3C8jI9Gn/LeiZyfH9MRSGSf4maQIDAQAB +AoGAHpeVtv62uarl9xKvuBBm0AwQmZnhq9HIsFaw5hMg8Vo7hbzFBvx1IirTOkC/ +u+QvfW1QLVFh6m3z4ySzV4fZBtcd6F9SbSrZ0xsxIUB2NOSa1RGgiaP61bJnMMM1 +xM3O9iwM5GZc3Gqy08QOCpDl0772VJ+9Gz3FA88mrc6rHQkCQQDz6RIatFjT28n8 +1vy0nHwwZz2oXTpe/pyZPwoKj8zVsmrKhKwOw7l8ArxjP8zoHOE7AQBCXYDMNoFp +IAF0yuqrAkEA6s0wMEdPpQeb0XHAfccyJQoeULxHdVFoz1wWmGSOm4YmQtR8/QJx +luEgfpeRkzxBKt5Ls3MEkheIOw7xV24zOwJAMz+DaE0AZPNHS3K4ghJnHZxzng6I +lzEUIjbWm0V/ml70hTy/EhMZw+6nOotLOHHo+QbK0SboSwAgzL/Gzo1cJQJANqpS +38qadleRJXAQWrg3qnvyluVe1aeAeVZ9RDmVIgxXeBO0jcs12uTLBe4PzHGo0mwy +v7K1i7XC180gzzQu5QJBAOxITT9RoWSSozPvnirHd37sn+RsrNYkV07NAa80M20Z +DkBPHeMVkNgigrQ6L6vWmbRDGQbGcMplAxnI5ppKCoU= +-----END RSA PRIVATE KEY----- diff --git a/demo/x509/x509auth.py b/demo/x509/x509auth.py new file mode 100644 index 0000000..a1f6e4b --- /dev/null +++ b/demo/x509/x509auth.py @@ -0,0 +1,675 @@ +#!/usr/bin/env python2 +# +# vim: ts=4 sw=4 nowrap +# +# ChannelHandler +# ReceiveChannel +# SendChannel +# +# SocketDispatcher +# Loop +# Poll +# ReadEvent +# WriteEvent +# +import sys, re, time, thread, os +import getopt +import exceptions +import select +import string +import socket +import StringIO +import traceback + + +from errno import EALREADY, EINPROGRESS, EWOULDBLOCK, ECONNRESET, ENOTCONN, ESHUTDOWN, EINTR, EISCONN + +STDOUT = sys.stdout + +IDstdin = 0 +IDstdout = 1 +IDstderr = 2 + + + + +class ExitNow (exceptions.Exception): + pass + + +class AuthError(exceptions.Exception): + def __init__ (self, msg): + self.msg = msg + def __str__ (self): + return repr( self.msg ) + +#---------------------------------------------------------------- +# +# Class Security Certification +# +#---------------------------------------------------------------- +import M2Crypto +import random +import base64 +import sha + +class CertHandler: + + def __init__ ( self ): + self.Nonce = None + self.CurrentObj = {} + #self.ServerName = socket.gethostbyaddr(socket.gethostname())[2][0] + self.ServerName = 'AuthInstance' + + self.ObjNames = { 'C' : 'countryName', 'ST' : 'stateOrProvinceName', 'L' : 'localityName', 'O' : 'organizationName', 'OU' : 'organizationalUnitName', 'CN' : 'commonName', 'email' : 'emailAddress' } + self.ObjMap = {} + + self.Params = { 'Digest' : 'sha1', + 'Version' : 1, + 'Serial' : 1, + 'NotBefore' : ( 60 * 60 * 24 * 365 ), + 'NotAfter' : ( 60 * 60 * 24 * 365 * 5 ), + 'Issuer' : { 'countryName' : 'US', 'stateOrProvinceName' : 'florida', 'localityName' : 'tampas', 'organizationName' : 'watersprings', 'organizationalUnitName' : 'security', 'commonName' : 'Certificate Authority', 'emailAddress' : 'admin@security' }, + 'Subject' : { 'countryName' : 'US', 'stateOrProvinceName' : 'florida', 'localityName' : 'miami', 'organizationName' : 'watersprings', 'organizationalUnitName' : 'security', 'commonName' : 'Certificate Authority', 'emailAddress' : 'admin@security' } + } + self.KeyEnv = { 'RsaPubKey' : [ '-----BEGIN PUBLIC KEY-----', '-----END PUBLIC KEY-----' ], + 'RsaPKey' : [ '-----BEGIN RSA PRIVATE KEY-----', '-----END RSA PRIVATE KEY-----' ], + 'X509Cert' : [ '-----BEGIN CERTIFICATE-----', '-----END CERTIFICATE-----' ] + } + self.CertContainer () + + self.ObjFromContainer ( ObjName='CA' ) + self.ObjFromContainer ( ObjName=self.ServerName ) + self.ServerObj = self.ObjMap[ self.ServerName ] + + + + + + def CertContainer (self): + self.PemMap = { 'CA' : { 'Subject' : {'organizationalUnitName': 'security', 'organizationName': 'watersprings', 'commonName': 'Certificate Authority', 'stateOrProvinceName': 'florida', 'countryName': 'US', 'emailAddress': 'admin@security', 'localityName': 'miami'}, + 'RsaPKey' : ['MIICXQIBAAKBgQDmAl+4+XdF34D3kBN58An29mA8/D+NUHVJW+XeE96uDJ9mw8f1', 'xguVYgfpMaiVihW/qDWZRu/NhWOfheKBVNstx5OcqIjY10vBvGAG17CQZhcon8eN', 'Kufg7XzON7e5WXXD8qyklhuesHtTEGGpZ1FfA+n+D/0JF3YfTBDeYyY2VQIDAQAB', 'AoGBAI53L/Uxx7fmzUoJ2pZvoKxwRIHhuDd+e3c5zbJ1WjsyJFWRtLw9tBUOCFpf', 'YM1nHzt8I97RulzxXxiC5B45ghu3S0s+B06oEOUxNLbjsai08DKBRFqM+IZIx11r', 'IM/tZsTdJg1KtKojRu63NDtOzR6a7ggTeMge5CDKpXVWpvVtAkEA+QF/q2NnsdmL', 'ak6ALl8QwMbvwujJcjLwvecHQJmB0jO9KF60Hh4VY8mTRIMZ/r9Wf+REsQcZtmhG', 'WRr12si5qwJBAOx4R0Wd/inoXOpvKheIoKgTg01FnLhT8uiLY59CuZRr6AcTELjC', 'Kvk6LyfhspYBkUwWAEwKxJ3kMeqXG+k8z/8CQQCy+GDKzqe5LKMHxWRb7/galuG9', 'NZOUgQiHdYXA6JRmgMl0Op07CGRXVIqEs7X7Y4rIYUj99ByG/muRn88VcTABAkBQ', 'Z6V0WoBtp4DQhfP+BIr8G4Zt49miI4lY4OyC3qFTgk1m+miZKgyKqeoW2Xtr3iSV', 'hnWbZZ3tQgZnCfKHoBHpAkAmf2OvfhLxaW1PwdjBdm9tFGVbzkLFDqdqww2aHRUx', 'sXonHyVG2EDm37qW7nzmAqUgQCueMhHREZQYceDrtLLO'], + 'X509Cert' : ['MIICpzCCAhCgAwIBAQIBATANBgkqhkiG9w0BAQUFADCBmTERMA8GA1UECxMIc2Vj', 'dXJpdHkxFTATBgNVBAoTDHdhdGVyc3ByaW5nczEeMBwGA1UEAxMVQ2VydGlmaWNh', 'dGUgQXV0aG9yaXR5MRAwDgYDVQQIEwdmbG9yaWRhMQswCQYDVQQGEwJVUzEdMBsG', 'CSqGSIb3DQEJARYOYWRtaW5Ac2VjdXJpdHkxDzANBgNVBAcTBnRhbXBhczAeFw0w', 'MzAzMzExMDQ2MDVaFw0wOTAzMjkxMDQ2MDVaMIGYMREwDwYDVQQLEwhzZWN1cml0', 'eTEVMBMGA1UEChMMd2F0ZXJzcHJpbmdzMR4wHAYDVQQDExVDZXJ0aWZpY2F0ZSBB', 'dXRob3JpdHkxEDAOBgNVBAgTB2Zsb3JpZGExCzAJBgNVBAYTAlVTMR0wGwYJKoZI', 'hvcNAQkBFg5hZG1pbkBzZWN1cml0eTEOMAwGA1UEBxMFbWlhbWkwgZ8wDQYJKoZI', 'hvcNAQEBBQADgY0AMIGJAoGBAOYCX7j5d0XfgPeQE3nwCfb2YDz8P41QdUlb5d4T', '3q4Mn2bDx/XGC5ViB+kxqJWKFb+oNZlG782FY5+F4oFU2y3Hk5yoiNjXS8G8YAbX', 'sJBmFyifx40q5+DtfM43t7lZdcPyrKSWG56we1MQYalnUV8D6f4P/QkXdh9MEN5j', 'JjZVAgMBAAEwDQYJKoZIhvcNAQEFBQADgYEAK7f4YodUnT7Ygp7BWBBDHSq/r+tY', 'H69ly3W23U5VupaIglNiNQoMqnZpVVfcuIYltajrux5TSH4gPbenCg163Ua8RvF6', 'E2JElccprbKiCf9tf8l6Rxpsall4EF+CazP56DiUD1NfGLhWp9V2ga9SoynEo1P1', 'eztMBfk01atBJ/s='] + }, + 'AuthInstance' : { 'Subject' : {'commonName': 'AuthInstance', 'organizationalUnitName': 'security', 'emailAddress': 'ioclient@AuthInstance'}, + 'RsaPKey' : ['MIICXQIBAAKBgQCwfB6CoOQTJTd6D4ua1G/H9hwqpdVUMMjG3O8Y93vYGesZdwtT', '1iEQX/6TWACBxa7jOC8hHUHe2lsPu7imHv8dDiD59Rzets7BM88HsJTemYrxSv5G', 'uh8FloB1KEtSHeCZSlDT/tzSX4M0JfVPmtx+0FsyDOVZ6jXjRIyIKgqDlwIDAQAB', 'AoGAYc8YFatXW6j3mwU8iL2NidPC/nvTxAoZa+UL+dlG4JhUrFNGitsUjf+1ljFi', 'bomBiFod/Is7c2euqgSOrDpnheYlogv2QpnP80YUpiv9OruaB9I1zqJ7QM7PrkrH', 'm1C36DzyzVY+4DMvTV29do4Mf6CKT8xf6hXlLK/NbqwO9NkCQQDYwxwCTWxrkX08', '+0c5KaTYxfqCByxOqoiKl97p6wHxNtlzdLeFoSZD0n3Q1c2v0DIXhcBPRPPaZBWC', 'yTayMkRzAkEA0G6I5mHQVNIx18Xmc75urC0QWrum9cj5VcyRvl3KCzB2kQoXkx6v', 'y0JN6YS37rSp8vmvIFNO/oHWSuEJlFYfTQJAajWv07D8Hvj61JaLH4c4Lr9TL8M0', 'Apesr7wajaOJIBgwFFJsWh3MEg9hdqJMVok9AimXQUAX/DpuD9dn5Yib4QJBAIdt', 'Kno2V7ylDkmahk/yDcrFRPkPMD5GpOrAjnnYSqzWglNe8U5gA+zXWfQ+jZwFut7q', 'qIUiXBM1nVzttuGwy4kCQQC3MHppypSWoFqd+TaxK3MX/HoZqaoRELXdeiniOt3Z', 'gFMJ4m6D9lL4segWDoDpequjDYxv2cl+wS1+qDOyeG3J'], + 'X509Cert' : ['MIIBxDCCAS2gAwIBAQIBATANBgkqhkiG9w0BAQUFADAAMB4XDTAzMDMzMTEwNTE1', 'N1oXDTA5MDMyOTEwNTE1N1owUDEkMCIGCSqGSIb3DQEJARYVaW9jbGllbnRAQXV0', 'aEluc3RhbmNlMREwDwYDVQQLEwhzZWN1cml0eTEVMBMGA1UEAxMMQXV0aEluc3Rh', 'bmNlMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCwfB6CoOQTJTd6D4ua1G/H', '9hwqpdVUMMjG3O8Y93vYGesZdwtT1iEQX/6TWACBxa7jOC8hHUHe2lsPu7imHv8d', 'DiD59Rzets7BM88HsJTemYrxSv5Guh8FloB1KEtSHeCZSlDT/tzSX4M0JfVPmtx+', '0FsyDOVZ6jXjRIyIKgqDlwIDAQABMA0GCSqGSIb3DQEBBQUAA4GBAFsXewlXnKpH', 'uSwxavmLPUqRk7o1D5E3ByTddBe3BY5NqEXk7Y2SJFtumiuUY5/sWB/aO8Xbqj/b', '/7Cwg9+bc9QqxeeIe/YvtFOmv1ELh2BC1Nof7zSa5rLa/+gPYCoogS4mLRMuUfRk', 'tVHhpoxL1B+UXp4jNeKeTgquOjpUiyBR'] + }, + '192.168.1.20' : { 'Subject' : {'commonName': '192.168.1.20', 'organizationalUnitName': 'security', 'emailAddress': 'ioclient@192.168.1.20'}, + 'RsaPKey' : ['MIICWwIBAAKBgQDD285BGY+FHhfpRvcqupN8X2lPwUNq4G7k5kit5cyuQLfN0+eQ', 'I+VFZdtfJhCZC54dEIvNgA4I7563pRUD0S9rmN6kh/M0GgrKZjYNO+CvvG2dts26', 'MGK0eUQaSsvDf9phEA+0mSv9dsUrdyBTJBn4mXvApekYHt+mNLfCVLkM1QIDAQAB', 'AoGAMqcFB3cJ0/59Zpowz/8ip3axcKvluJ1EcLRRtY+JyML6BiQ4beGqqLD38/qP', 'LlV/1bpyvXnRp2P5IztxXORbo77IzDVzl62YesQATnecSCMLTaeOusy2EZZsjE0k', 'V2cR1rZvzyJPY+Fi8X54hiB+5IcKkPRX9LVw7+yBbBh4sKECQQD0Yi0/DGa3IetR', '9F+/jgN/VIcTd5KwMBW3Bw/+Bh78ZlZGaucpRiR1IQuD7sLTnhNS6RMJUxv10jnS', 'BGW9pjX5AkEAzSslOGFyJ5Aoy48rgC2kKwq6nFKJ/PmY92cnm0nqmwb2npbOtDxz', 'sPUdb7oYmUU/nVCJh3yb+KJIw2g9XxnUvQJAG8ybNwPTH1vlZ+Izjhe6gB5+axF8', 'BzzBC5vrDstldPKzN7lraD+JYCWNKMndMbNWoWTP/IyOrqzmVOSZKjShCQJAbzuE', 'C2QxaqeqpmnxkKWuCrPfZl8NdryvpPolK/jQG8qTrHlgibD4nCjYE7nWGkrD6Xs/', 'hNgXC56YSnDaTRQJFQJAD5GFACv9QgcMZhy1hza0yGDMSQ0WR8/y3CJhi3DPOuAf', 'MetGM1kLQR8bDFrl7yEs+Nufk8QTsE5ngZ7dGFgmuA=='], + 'X509Cert' : ['MIIBxDCCAS2gAwIBAQIBATANBgkqhkiG9w0BAQUFADAAMB4XDTAzMDMzMTEwNTMw', 'NVoXDTA5MDMyOTEwNTMwNVowUDEkMCIGCSqGSIb3DQEJARYVaW9jbGllbnRAMTky', 'LjE2OC4xLjIwMREwDwYDVQQLEwhzZWN1cml0eTEVMBMGA1UEAxMMMTkyLjE2OC4x', 'LjIwMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDD285BGY+FHhfpRvcqupN8', 'X2lPwUNq4G7k5kit5cyuQLfN0+eQI+VFZdtfJhCZC54dEIvNgA4I7563pRUD0S9r', 'mN6kh/M0GgrKZjYNO+CvvG2dts26MGK0eUQaSsvDf9phEA+0mSv9dsUrdyBTJBn4', 'mXvApekYHt+mNLfCVLkM1QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBAA5pWiXJOHCJ', 'P6kHcBITJuwv94zZ0dbHby1ljUfG/8z3Fmr8JRUcTuTtgVYH9M0O9QujIR8YLWSV', '0GeD3nkLRn0+ezam0CW0dF/Ph5vNXFP4a0DSEVv7T0G21VFmbUV3xrVeaXARFuLa', 'AtqRoSyBMajd3g0WNXDCgGEH7LvzJ5EP'] + }, + '192.168.1.26' : { 'Subject' : {'commonName': '192.168.1.26', 'organizationalUnitName': 'security', 'emailAddress': 'ioclient@192.168.1.26'}, + 'RsaPKey' : ['MIICXQIBAAKBgQC5ILRHC3wFoqG9Egb96N3iGEnVrgvQikHyXYc/jFMUgB79rVJp', 'hY1MziGkSjSyc3RFMshkjHlMlARMPCNtomIikqAQaO4Eke2SYWyaOBoTdkeOy+yZ', 't/POpoGp3nRmKGed6NNcdMd5BO01GiatUb7X/Se3Yyvmj5UcEmv/hZQGFwIDAQAB', 'AoGBALdR5FMp0zE9X437iQLsErQuOwcmpzplfnJDHYfXK/nz+TxY4m/tuQNiZ7vp', 'Y4+Gdo+Dfx7aX89uD2dycd7B2wwTziBGIEjhusD8gtralVjhBDjCowSOkezWTeY+', '2h40NB4e1uypOZb0PXWvAL/l9xN7NBGioq9zmShT5c+FFO8RAkEA4k3QSaT1ScGI', '5II5JolvPnv6yS+0dCQTn1SC2ABWbH75NDUHMGAdNIf1sqhaLSQnY9GuXhb8XqX6', 'UUhoypUHzwJBANFrqnuEuTNKR0HVD31/2trPYLfZL6/9RUsR4mlvxPb0tX+T5LVL', '5he43zbura/lZqNxt0ZVeD03LanPN7bvZzkCQQCMAToIJa6+x6YKQOpchhA1pvwb', 'NZE9fQhKvT0JpwPQsak4/EmLSxsmYarGsdLANKrN3W4ztaLCZ4r6eIKkOhkPAkBz', 'ke4wYitucbRnUTRONuvJSx599x6JCcVey0zekO7qtlsfP7e8kVk2iDCu+QLjCj8d', 'Pdk9uFc1uSi7CH8ftniJAkBLNYF0kfGC+CaTuyfnIwiBZ/tjmm4UvHfwtlaZHJYc', 'QIjimBxVA7mujrv3xIBTiDMdxUhq9YIaKIEdlveaTwPK'], + 'X509Cert' : ['MIIBxDCCAS2gAwIBAQIBATANBgkqhkiG9w0BAQUFADAAMB4XDTAzMDMzMTEwNTQx', 'NloXDTA5MDMyOTEwNTQxNlowUDEkMCIGCSqGSIb3DQEJARYVaW9jbGllbnRAMTky', 'LjE2OC4xLjI2MREwDwYDVQQLEwhzZWN1cml0eTEVMBMGA1UEAxMMMTkyLjE2OC4x', 'LjI2MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC5ILRHC3wFoqG9Egb96N3i', 'GEnVrgvQikHyXYc/jFMUgB79rVJphY1MziGkSjSyc3RFMshkjHlMlARMPCNtomIi', 'kqAQaO4Eke2SYWyaOBoTdkeOy+yZt/POpoGp3nRmKGed6NNcdMd5BO01GiatUb7X', '/Se3Yyvmj5UcEmv/hZQGFwIDAQABMA0GCSqGSIb3DQEBBQUAA4GBAMPd5SXzpwZ+', '40SdOv/PeQ5cjieDm6QjndWE/T8nG2I5h6YRWbZPohsCClQjrTyZCMXwcUiCimuJ', 'BaMigI/YqP5THVv58Gu8DpoVZppz7uhUNS5hsuV9lxZUh1bRkUtL6n0qSTEdM34I', 'NJBJKGlf0skULg9BT4LJYTPGWJ0KosUl'] + } + } + + def CreateObj ( self, ObjName='CA' ): + self.ObjMap[ObjName] = { 'RsaPKey' : None, + 'PsaPubKey' : None, + 'EvpPKey' : None, + 'EvpPubKey' : None, + 'X509Req' : None, + 'X509Cert' : None, + } + self.CurrentObj = self.ObjMap[ObjName] + + def ObjFromContainer (self, ObjName='CA' ): + if not self.ObjMap.has_key( ObjName ): + self.CreateObj ( ObjName=ObjName ) + self.PKeyFromPemRepr ( ObjName=ObjName ) + self.CertFromPemRepr ( ObjName=ObjName ) + + def PKeyFromPemRepr ( self, ObjName=None, PemPKey=None ): + def callback (): return '' + if self.PemMap.has_key( ObjName ): + UsedPemPKey = self.KeyEnv['RsaPKey'][0] + '\n' + string.join( self.PemMap[ObjName]['RsaPKey'], '\n' ) + '\n' + self.KeyEnv['RsaPKey'][1] + '\n' + else: + if not PemPKey: + raise AuthError( 'no such Object "%s" in container - abort!' % ObjName ) + else: + UsedPemPKey = PemPKey + self.CurrentObj['RsaPKey'] = M2Crypto.RSA.load_key_string( UsedPemPKey, callback ) + self.CurrentObj['EvpPKey'] = M2Crypto.EVP.PKey ( md='sha1' ) + self.CurrentObj['EvpPKey'].assign_rsa ( self.CurrentObj['RsaPKey'] ) + self.CurrentObj['RsaPubKey'] = M2Crypto.RSA.new_pub_key( self.CurrentObj['RsaPKey'].pub () ) + + def CertFromPemRepr ( self, ObjName=None, PemCert=None ): + if self.PemMap.has_key( ObjName ): + UsedPemCert = self.KeyEnv['X509Cert'][0] + '\n' + string.join( self.PemMap[ObjName]['X509Cert'], '\n' ) + '\n' + self.KeyEnv['X509Cert'][1] + '\n' + else: + UsedPemCert = PemCert + self.CurrentObj['X509Cert'] = M2Crypto.X509.load_cert_string( PemCert ) + self.CurrentObj['EvpPubKey'] = self.CurrentObj['X509Cert'].get_pubkey () + #self.CurrentObj['RsaPubKey'] = M2Crypto.RSA.rsa_from_pkey( self.CurrentObj['EvpPubKey'] ) + + def ObjNameFromPemCert ( self, PemCert=None ): + """ + generate objmap structure and fill it with values from PemCert + return ObjName string + """ + X509Cert = M2Crypto.X509.load_cert_string( PemCert ) + Subject = X509Cert.get_subject () + SubjectTxt = Subject.print_ex () + SubjectTxtList = re.split('[\n\r]', SubjectTxt ) + SubjectMap = {} + for Entry in SubjectTxtList: + ( Key, Value ) = re.split('=', Entry) + if self.ObjNames.has_key( Key ): + SubjectMap[ self.ObjNames[Key] ] = Value + else: + SubjectMap[ Key ] = Value + if not SubjectMap.has_key( 'commonName' ): + return False + ObjName = SubjectMap['commonName'] + if not self.ObjMap.has_key( ObjName ): + self.CreateObj( ObjName=ObjName ) + self.CurrentObj = self.ObjMap[ObjName] + self.CurrentObj['X509Cert'] = X509Cert + self.CurrentObj['EvpPubKey'] = self.CurrentObj['X509Cert'].get_pubkey () + self.CurrentObj['RsaPubKey'] = M2Crypto.RSA.rsa_from_pkey( self.CurrentObj['EvpPubKey'] ) + else: + self.CurrentObj = self.ObjMap[ ObjName ] + return ObjName + + + + def ServerCert ( self ): + self.ObjFromContainer( ObjName='X509Auth' ) + + + def CreatePKey ( self ): + def PassPhraseFkt (): return '' + RsaKeyParams = { 'KeyLength' : 1024, + 'PubExponent' : 0x10001, # -> 65537 + 'keygen_callback' : PassPhraseFkt + } + self.CurrentObj['RsaPKey'] = M2Crypto.RSA.gen_key( RsaKeyParams['KeyLength'], RsaKeyParams['PubExponent'], RsaKeyParams['keygen_callback'] ) + self.CurrentObj['EvpPKey'] = M2Crypto.EVP.PKey ( md=self.Params['Digest'] ) + self.CurrentObj['EvpPKey'].assign_rsa ( self.CurrentObj['RsaPKey'] ) + #print self.EvpPKey + + + def CreateCert ( self, SignEvpPKey=None ): + """ + generate new x509 certificate + SignEvpKey pkey to sign x509 certification, if None this x509 cert will be self signed + """ + self.CurrentObj = self.ObjMap['CA'] + self.CreatePKey () + X509Cert = M2Crypto.X509.X509 () + X509Cert.set_version ( self.Params['Version'] ) + X509Cert.set_serial_number ( self.Params['Serial'] ) + X509Cert.set_not_before ( int( time.time() - self.Params['NotBefore'] )) # 1 year in the past + X509Cert.set_not_after ( int( time.time() + self.Params['NotAfter'] )) # 5 years in the future + X509Cert.set_issuer ( self.Params['Issuer'] ) + X509Cert.set_subject ( self.Params['Subject'] ) + X509Cert.set_pubkey ( self.CurrentObj['EvpPKey'] ) + if SignEvpPKey: + X509Cert.sign ( SignEvpPKey, self.Params['Digest'] ) + else: + X509Cert.sign ( self.CurrentObj['EvpPKey'], self.Params['Digest'] ) + self.CurrentObj['X509Cert'] = X509Cert + self.DumpOutInternalPemRepr( ObjName='CA' ) + + + def CreateObjCert (self, ObjName): + """ + generate Obj with new PKey and Request, signed by 'CA' + ObjName the primary key to identify key-pair + """ + # new obj + if not self.ObjMap.has_key( ObjName ): + self.ObjMap[ObjName] = {} + self.CurrentObj = self.ObjMap[ObjName] + if not self.CurrentObj.has_key( 'Subject' ): + self.CurrentObj['Subject'] = { 'organizationalUnitName' : 'security', 'commonName' : ObjName, 'emailAddress' : 'ioclient@' + ObjName } + # new pkey + self.CreatePKey () + # new request + self.CreateReq ( SignEvpPKey=self.ObjMap['CA']['EvpPKey'] ) + # new certification + if not self.Req2Cert ( SignEvpPKey=self.ObjMap['CA']['EvpPKey'] ): + print "300 error occured while verifying - abort!" + # shipout x509 certification + self.DumpOutInternalPemRepr( ObjName=ObjName ) + + + def CreateReq ( self, SignEvpPKey=None ): + X509Req = M2Crypto.X509.Request () + if self.Params['Version']: + X509Req.set_version ( self.Params['Version'] ) + X509Req.set_subject ( self.CurrentObj['Subject'] ) + X509Req.set_pubkey ( self.CurrentObj['EvpPKey'] ) + if SignEvpPKey: + X509Req.sign ( SignEvpPKey, self.Params['Digest'] ) + else: + X509Req.sign ( self.CurrentObj['EvpPKey'], self.Params['Digest'] ) + self.CurrentObj['X509Req'] = X509Req + + + def Req2Cert ( self, SignEvpPKey=None ): + X509Cert = M2Crypto.X509.X509 () + Version = self.CurrentObj['X509Req'].get_version () + X509Cert.set_version ( Version ) + X509Cert.set_serial ( self.Params['Serial'] ) + X509Cert.set_not_before ( int( time.time() - self.Params['NotBefore'] )) # 1 year in the past + X509Cert.set_not_after ( int( time.time() + self.Params['NotAfter'] )) # 5 years in the future + Issuer = self.ObjMap['CA']['X509Cert'].get_issuer () + X509Cert.set_issuer_name ( Issuer ) + X509Name_Subject = self.CurrentObj['X509Req'].get_subject () + X509Cert.set_subject_name ( X509Name_Subject ) + PKey = self.CurrentObj['X509Req'].get_pubkey () + EvpPKey = M2Crypto.EVP.PKey( PKey ) + X509Cert.set_pubkey ( EvpPKey ) + if SignEvpPKey: + X509Cert.sign ( SignEvpPKey, self.Params['Digest'] ) + else: + X509Cert.sign ( self.CurrentObj['EvpPKey'], self.Params['Digest'] ) + self.CurrentObj['X509Cert'] = X509Cert + if self.VerifyCert ( SignEvpPKey ): + return True + else: + return False + + + + + #-------------------------------- + # CertHandler Verifying + #-------------------------------- + def ExtractPublicKeyFromCert ( self ): + self.CurrentObj['EvpPubKey'] = self.CurrentObj['X509Cert'].get_pubkey () + + def VerifyCert ( self, EvpPKey ): + if dir(EvpPKey).count('_ptr'): + Result = self.CurrentObj['X509Cert'].verify ( EvpPKey._ptr() ) + else: + Result = self.CurrentObj['X509Cert'].verify ( EvpPKey ) + if Result: + return True + return False + + + + #-------------------------------- + # CertHandler DumpOut + #-------------------------------- + def DumpOutInternalPemRepr( self, ObjName='unknown', File='PyReprPem.txt' ): + if File: + open( File, 'w').write("\t\t\t\t'%s' : { " % ( ObjName )) + else: + sys.stdout.write("\t\t\t\t'%s' : { " % ( ObjName )) + self.ShowX509CertSubject ( File ) + self.RsaPKey2PemRepr ( File, Cipher=None ) # unprotectd pkey representation + self.X509Cert2PemRepr ( File ) + if File: + open( File, 'a').write("\t\t\t\t\t }\n") + else: + sys.stdout.write("\t\t\t\t\t }\n") + + def ShowX509CertIssuer ( self, File=None ): + IssuerName = self.CurrentObj['X509Cert'].get_issuer () + print IssuerName.print_ex () + + def ShowX509CertSubject ( self, File=None ): + Subject = self.CurrentObj['X509Cert'].get_subject () + SubjectTxt = Subject.print_ex () + SubjectTxtList = re.split('[\n\r]', SubjectTxt ) + SubjectMap = {} + for Entry in SubjectTxtList: + ( Key, Value ) = re.split('=', Entry) + if self.ObjNames.has_key( Key ): + SubjectMap[ self.ObjNames[Key] ] = Value + else: + SubjectMap[ Key ] = Value + if File: + open( File, 'a').write("'Subject' : %s,\n" % ( repr( SubjectMap ) )) + else: + sys.stdout.write("Subject: %s\n" % ( repr( SubjectMap ) )) + + def RsaPKey2PemRepr ( self, File=None, Cipher=None ): + """ + converting pkey to PEM representation + Cipher if set to None, the pkey will be unprotected!!!!! possible other value: 'des_ede3_cbc' + """ + PemRsaPKey = self.CurrentObj['RsaPKey'].repr_key_pem ( cipher=Cipher ) + PemRsaPKeyList = re.split('[\n\r]', PemRsaPKey) + if File: + open( File, 'a').write("\t\t\t\t\t\t\t\t'RsaPKey' : %s,\n" % ( repr(PemRsaPKeyList[1:-2]) )) + else: + sys.stdout.write("\t\t\t\t\t\t\t\t'RsaPKey' : %s,\n" % ( repr(PemRsaPKeyList[1:-2]) )) + + def X509Cert2PemRepr ( self, File=None ): + PemCert = self.CurrentObj['X509Cert'].repr_cert_pem () + #print PemCert + PemCertList = re.split('[\n\r]', PemCert) + if File: + open( File, 'a').write("\t\t\t\t\t\t\t\t'X509Cert' : %s\n" % ( repr(PemCertList[1:-2]) )) + else: + sys.stdout.write("\t\t\t\t\t\t\t\t'X509Cert' : %s\n" % ( repr(PemCertList[1:-2]) )) + + + + #-------------------------------- + # CertHandler encryption / decryption + #-------------------------------- + def CreateNonce ( self ): + """ + creating some randomised data + return new Nonce string + """ + random.seed () + RawNonce = "%s_%f_%f" % ( os.getpid(), time.time(), random.random() ) + sha1=M2Crypto.EVP.MessageDigest('sha1') + sha1.update( RawNonce ) + NonceDecrypted = sha1.digest() + return NonceDecrypted + + def NonceEncryptPrivate ( self, NonceDecrypted, RsaPKey=None ): + """ + creating private encrypted string from NonceDecrypted + """ + padding = M2Crypto.RSA.pkcs1_padding + if not RsaPKey: + UsedRsaPKey = self.ServerObj['RsaPKey'] + else: + UsedRsaPKey = RsaPKey + NoncePrivEncrypted = UsedRsaPKey.private_encrypt ( NonceDecrypted, padding ) + return NoncePrivEncrypted + + def NonceEncryptPublic ( self, NonceDecrypted, RsaPubKey=None ): + """ + creating public encrypted string from NonceDecrypted + """ + padding = M2Crypto.RSA.pkcs1_padding + if not RsaPubKey: + UsedRsaPubKey = self.ServerObj['RsaPubKey'] + else: + UsedRsaPubKey = RsaPubKey + NoncePubEncrypted = UsedRsaPubKey.public_encrypt ( NonceDecrypted, padding ) + return NoncePubEncrypted + + def NonceDecryptPublic ( self, NoncePrivEncrypted, RsaPubKey=None ): + """ + creating decrypted string from NoncePrivEncrypted + """ + padding = M2Crypto.RSA.pkcs1_padding + if not RsaPubKey: + UsedRsaPubKey = self.ServerObj['RsaPubKey'] + else: + UsedRsaPubKey = RsaPubKey + try: + NonceDecrypted = UsedRsaPubKey.public_decrypt ( NoncePrivEncrypted, padding ) + except: + raise AuthError('decrypting of public key failed - abort!') + return NonceDecrypted + + def NonceDecryptPrivate ( self, NoncePubEncrypted, RsaPKey=None ): + padding = M2Crypto.RSA.pkcs1_padding + if not RsaPKey: + UsedRsaPKey = self.ServerObj['RsaPKey'] + else: + UsedRsaPKey = RsaPKey + NonceDecrypted = UsedRsaPKey.private_decrypt ( NoncePubEncrypted, padding ) + return NonceDecrypted + + def NonceVerify ( self, DecryptedNonce=None ): + if self.CurrentObj['Nonce']['Decrypted'] == DecryptNonce: + return True + return False + + #-------------------------------- + # CertHandler authentication request + #-------------------------------- + def ClientInit ( self, ObjName=None ): + """ + generating AuthString + Nonce messagedigest 'sha1', encrypted with own instance private key + Cert own instance X509 cert, PEM encoded + any linefeed charaters stripped out of the base64 code + return generated Nonce and AuthString + """ + if ObjName: + if self.PemMap.has_key( ObjName ): + UsedObjName = ObjName + else: + UsedObjName = self.ServerName + else: + UsedObjName = self.ServerName + + NonceDecrypted = self.CreateNonce () + NoncePrivEncrypted = re.sub('\012', '', base64.encodestring( self.NonceEncryptPrivate ( NonceDecrypted, RsaPKey=self.ServerObj['RsaPKey'] )) ) + PemCert = re.sub('\012', '', base64.encodestring( self.KeyEnv['X509Cert'][0] + '\n' + string.join( self.PemMap[UsedObjName]['X509Cert'], '\n' ) + '\n' + self.KeyEnv['X509Cert'][1] + '\n' )) + InitString = re.sub('\012', '', base64.encodestring('%s:%s' % ( NoncePrivEncrypted, PemCert ))) + return ( NonceDecrypted, InitString ) + + + def ClientInitVerify ( self, InitString ): + """ + return decrypted Nonce from AuthString and ObjName from AuthString X509 Cert + """ + try: + PemBaseString = base64.decodestring( InitString ) + except base64.binascii.Error, msg: + raise base64.binascii.Error( msg ) + try: + ( Base64Nonce, Base64Cert ) = re.split(':', PemBaseString ) + except: + raise AuthError( 'cannot split PemBaseString into parts - abort!' ) + try: + NoncePrivEncrypted = base64.decodestring( Base64Nonce ) + except base64.binascii.Error, msg: + raise base64.binascii.Error( msg ) + try: + PemCert = base64.decodestring( Base64Cert ) + except base64.binascii.Error, msg: + raise base64.binascii.Error( msg ) + try: + X509Cert = M2Crypto.X509.load_cert_string( PemCert ) + except: + raise AuthError( 'cannot extract X509 cert from PEM representation - abort!' ) + EvpPKey = self.ObjMap['CA']['EvpPKey'] + if dir(EvpPKey).count('_ptr'): + Result = X509Cert.verify ( EvpPKey._ptr() ) + else: + Result = X509Cert.verify ( EvpPKey ) + if Result != 1: + raise AuthError( 'verification of X509 cert with Certification Authority "CA" failed with code %d - abort!' % ( Result )) + ClientObjName = self.ObjNameFromPemCert( PemCert=PemCert ) + try: + NonceDecrypted = self.NonceDecryptPublic( NoncePrivEncrypted, RsaPubKey=self.CurrentObj['RsaPubKey'] ) + except: + raise AuthError( 'wrong public key for encoding nonce - abort!' ) + + return ( NonceDecrypted, ClientObjName ) + + + + + def ServerInit ( self, ClientObjName, ClientNonce ): + """ + NonceServer new Nonce from server encrypted with client publickey and base64 encoded + NonceBounce the authrequest nonce encrypted with server privatekey and base64 encoded + PemServerCert server X509 certification PEM encoded and base64 encoded + """ + if not self.ObjMap.has_key( ClientObjName ): + if not self.PemMap.has_key( ClientObjName ): + raise AuthError( 'cannot find ClientObjName - abort!' ) + else: + self.ObjFromContainer( ObjName=ClientObjName ) + else: + self.CurrentObj = self.ObjMap[ClientObjName] + + NonceDecrypted = self.CreateNonce () + NonceServer = re.sub('\012', '', base64.encodestring( self.NonceEncryptPublic ( NonceDecrypted, RsaPubKey=self.CurrentObj['RsaPubKey'] )) ) + NonceBounce = re.sub('\012', '', base64.encodestring( self.NonceEncryptPublic ( ClientNonce, RsaPubKey=self.CurrentObj['RsaPubKey'] )) ) + PemServerCert = re.sub('\012', '', base64.encodestring( self.KeyEnv['X509Cert'][0] + '\n' + string.join( self.PemMap[self.ServerName]['X509Cert'], '\n' ) + '\n' + self.KeyEnv['X509Cert'][1] + '\n' ) ) + + InitString = re.sub('\012', '', base64.encodestring('%s:%s:%s' % ( NonceServer, NonceBounce, PemServerCert )) ) + return ( NonceDecrypted, InitString ) + + + def ServerInitVerify ( self, InitString, ObjName=None ): + NonceDecrypted = '' + ObjName = '' + try: + PemBaseString = base64.decodestring( InitString ) + except: + return False + + ( NonceServer, NonceBounce, ServerCert ) = re.split(':', PemBaseString ) + NoncePubServer = base64.decodestring( NonceServer ) # NonceServer + NoncePubBounce = base64.decodestring( NonceBounce ) # NonceBounce + PemServerCert = base64.decodestring( ServerCert ) # PemServerCert + + try: + X509Cert = M2Crypto.X509.load_cert_string( PemServerCert ) + except: + return False + + # verify X509 cert + EvpPKey = self.ObjMap['CA']['EvpPKey'] + if dir(EvpPKey).count('_ptr'): + Result = X509Cert.verify ( EvpPKey._ptr() ) + else: + Result = X509Cert.verify ( EvpPKey ) + if not Result: + return False + + # verify Nonce from Server encrypted with my own publickey + try: + NonceDecrypted = self.NonceDecryptPrivate( NoncePubServer, RsaPKey=self.ServerObj['RsaPKey'] ) + except: + return False + + ServerObjName = self.ObjNameFromPemCert( PemCert=PemServerCert ) + + # verify Nonce bounced from Server encrypted with server privatekey + try: + NonceBounceDecrypted = self.NonceDecryptPrivate( NoncePubBounce, RsaPKey=self.CurrentObj['RsaPKey'] ) + except: + return False + + return ( NonceDecrypted, NonceBounceDecrypted, ServerObjName ) + + + + def ReplyInit ( self, ReplyObjName, ReplyBounce ): + NonceDecrypted = self.CreateNonce () + NoncePubInit = re.sub('\012', '', base64.encodestring( self.NonceEncryptPublic ( NonceDecrypted, RsaPubKey=self.ObjMap[ ReplyObjName ]['RsaPubKey'] )) ) + NoncePubBounce = re.sub('\012', '', base64.encodestring( self.NonceEncryptPublic ( ReplyBounce, RsaPubKey=self.ObjMap[ ReplyObjName ]['RsaPubKey'] )) ) + ReplyString = re.sub('\012', '', base64.encodestring('%s:%s' % ( NoncePubInit, NoncePubBounce )) ) + return ( NonceDecrypted, ReplyString ) + + + def ReplyVerify ( self, ReplyString ): + try: + PemBaseString = base64.decodestring( ReplyString ) + except base64.binascii.Error, msg: + raise base64.binascii.Error( msg ) + ( NoncePubInit, NoncePubBounce ) = re.split(':', PemBaseString ) + + try: + NoncePubInit = base64.decodestring( NoncePubInit ) # new Nonce from Remote, encrypted with own publickey + except base64.binascii.Error, msg: + raise base64.binascii.Error( msg ) + try: + NoncePubBounce = base64.decodestring( NoncePubBounce ) # bounced Nonce from Remote, encrypted with Remote privatekey + except base64.binascii.Error, msg: + raise base64.binascii.Error( msg ) + + # verify Nonce from Remote encrypted with my own publickey + try: + NonceRemote = self.NonceDecryptPrivate( NoncePubInit, RsaPKey=self.ServerObj['RsaPKey'] ) + except: + raise AuthError( 'cannot encode nonce with own private key - abort!' ) + + # verify Nonce bounced from Remote encrypted with Remote privatekey + try: + NonceBounced = self.NonceDecryptPrivate( NoncePubBounce, RsaPKey=self.ServerObj['RsaPKey'] ) + except: + raise AuthError( 'wrong public key for encoding nonce - abort!' ) + + return ( NonceRemote, NonceBounced ) + + + + + #------------------------------------------------------------------------------------------- + # TEST + #------------------------------------------------------------------------------------------- + def CreateCAForContainer ( self ): + self.CreateCert () + + def CreateForContainer ( self, ObjName ): + """ + create new pkey pair and x509 cert for specified objname + result will be written in file "PyReprPem.txt" + """ + self.ObjFromContainer ( ObjName='AuthInstance' ) + self.CreateObjCert ( ObjName=ObjName ) + + + def Test ( self ): + #self.CreateCert () + #self.ExtractPublicKeyFromCert () + #self.VerifyCert () + + ( ClientInitNonce, ClientInitString ) = self.ClientInit () + ( ClientSendNonce, ClientObjName ) = self.ClientInitVerify ( InitString=ClientInitString ) + ( ServerInitNonce, ServerInitString ) = self.ServerInit ( ClientObjName=ClientObjName, ClientNonce=ClientSendNonce ) + ( ServerSendNonce, ClientBounceNonce, ServerObjName ) = self.ServerInitVerify ( InitString=ServerInitString ) + if ClientInitNonce == ClientBounceNonce : + print '100 Test Nonce bounced True' + else: + print '100 Test Nonce bounced False - abort!' + + ( ReplyInitNonce, ReplyInitString ) = self.ReplyInit ( ReplyObjName=ClientObjName, ReplyBounce=ServerSendNonce ) + ( ReplySendNonce, NonceBounced ) = self.ReplyVerify ( ReplyString=ReplyInitString ) + if ServerInitNonce == NonceBounced : + print '100 Test Nonce bounced True' + else: + print '100 Test Nonce bounced False - abort!' + + ( Reply2InitNonce, Reply2InitString ) = self.ReplyInit ( ReplyObjName=ClientObjName, ReplyBounce=ServerSendNonce ) + ( Reply2SendNonce, Nonce2Bounced ) = self.ReplyVerify ( ReplyString=Reply2InitString ) + if ServerInitNonce == Nonce2Bounced : + print '100 Test Nonce bounced True' + else: + print '100 Test Nonce bounced False - abort!' + + #self.NonceEncryptPrivate () + #self.NonceDecryptPublic () + #self.NonceVerify () + + +#----------------------------------------------------------------------------------------------- +# MAIN +# +# x509auth.py --ca +# will create a file "PyReprPem.txt" in the current directory +# append the contents of the file to the CertContainer in this script +# +# x509auth.py --cert <ObjName> +# creates a file "PyReprPem.txt" in the current directory +# append the contents of the file to the CertContainer in this script +# +# x509auth.py --test +# running authentification tests with bounced nonce +# +#----------------------------------------------------------------------------------------------- +if __name__ == '__main__': + run = CertHandler () + + if len( sys.argv ) > 1: + if sys.argv[1] == '--test': + run.Test () + elif sys.argv[1] == '--ca': + run.CreateCert() + elif sys.argv[1] == '--cert': + run.CreateForContainer( sys.argv[2] ) + + sys.exit( 0 ) + diff --git a/doc/ZServerSSL-HOWTO.html b/doc/ZServerSSL-HOWTO.html new file mode 100644 index 0000000..78c15a9 --- /dev/null +++ b/doc/ZServerSSL-HOWTO.html @@ -0,0 +1,271 @@ +<?xml version="1.0" encoding="utf-8" ?> +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> +<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> +<head> +<base href="http://localhost:9080/home/m2/zserverssl-011-howto/" /> + +<meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> +<meta name="generator" content="Docutils 0.2.8: http://docutils.sourceforge.net/" /> +<title>ZServerSSL HOWTO</title> +<meta name="author" content="Ng Pheng Siong" /> +<meta name="date" content="2003-06-22" /> +<link rel="stylesheet" href="default.css" type="text/css" /> +</head> +<body> +<div class="document" id="zserverssl-howto"> +<h1 class="title">ZServerSSL HOWTO</h1> +<table class="docinfo" frame="void" rules="none"> +<col class="docinfo-name" /> +<col class="docinfo-content" /> +<tbody valign="top"> +<tr><th class="docinfo-name">Author:</th> +<td>Ng Pheng Siong</td></tr> +<tr class="field"><th class="docinfo-name">Id:</th><td class="field-body">ZServerSSL-HOWTO,v 1.1 2003/06/22 17:40:13 ngps Exp</td> +</tr> +<tr><th class="docinfo-name">Date:</th> +<td>2003-06-22</td></tr> +<tr class="field"><th class="docinfo-name">Web-Site:</th><td class="field-body"><a class="reference" href="http://chandlerproject.org/Projects/MeTooCrypto">http://chandlerproject.org/Projects/MeTooCrypto</a></td> +</tr> +</tbody> +</table> +<div class="contents topic" id="contents"> +<p class="topic-title"><a name="contents">Contents</a></p> +<ul class="simple"> +<li><a class="reference" href="#introduction" id="id2" name="id2">Introduction</a></li> +<li><a class="reference" href="#preparation" id="id3" name="id3">Preparation</a></li> +<li><a class="reference" href="#installation" id="id4" name="id4">Installation</a></li> +<li><a class="reference" href="#testing" id="id5" name="id5">Testing</a><ul> +<li><a class="reference" href="#https" id="id6" name="id6">HTTPS</a></li> +<li><a class="reference" href="#webdav-over-https" id="id7" name="id7">WebDAV-over-HTTPS</a></li> +<li><a class="reference" href="#webdav-source-over-https" id="id8" name="id8">WebDAV-Source-over-HTTPS</a></li> +<li><a class="reference" href="#python-with-m2crypto" id="id9" name="id9">Python with M2Crypto</a><ul> +<li><a class="reference" href="#id1" id="id10" name="id10">HTTPS</a></li> +<li><a class="reference" href="#xmlrpc-over-https" id="id11" name="id11">XMLRPC-over-HTTPS</a></li> +</ul> +</li> +</ul> +</li> +<li><a class="reference" href="#conclusion" id="id12" name="id12">Conclusion</a></li> +</ul> +</div> +<div class="section" id="introduction"> +<h1><a class="toc-backref" href="#id2" name="introduction">Introduction</a></h1> +<p>ZServerSSL adds to Zope's ZServer the following:</p> +<ul class="simple"> +<li>HTTPS server</li> +<li>WebDAV-source-over-HTTPS server</li> +</ul> +<p>With the HTTPS server, ZServerSSL also provides WebDAV-over-HTTPS +and XMLRPC-over-HTTPS access to Zope.</p> +<p>These instructions apply to both Un*x and Windows installations of +Zope 2.6.1. To avoid cluttering the presentation, Windows pathnames +are shown in Un*x fashion.</p> +</div> +<div class="section" id="preparation"> +<h1><a class="toc-backref" href="#id3" name="preparation">Preparation</a></h1> +<ol class="arabic simple"> +<li>Download M2Crypto 0.11, contained in the file <tt class="literal"><span class="pre">m2crypto-0.11.zip</span></tt>.</li> +<li>Unpack <tt class="literal"><span class="pre">m2crypto-0.11.zip</span></tt>. This will create a directory +<tt class="literal"><span class="pre">m2crypto-0.11</span></tt>. Henceforth, we refer to this directory as <tt class="literal"><span class="pre">$M2</span></tt>.</li> +<li>Install M2Crypto per the instructions in <tt class="literal"><span class="pre">$M2/INSTALL</span></tt>.</li> +</ol> +<p>The ZServerSSL distribution is in <tt class="literal"><span class="pre">$M2/demo/Zope</span></tt>. We shall refer to +this directory as <tt class="literal"><span class="pre">$ZSSL</span></tt>.</p> +</div> +<div class="section" id="installation"> +<h1><a class="toc-backref" href="#id4" name="installation">Installation</a></h1> +<p>Below, we refer to your Zope top-level directory as <tt class="literal"><span class="pre">$ZOPE</span></tt>.</p> +<ol class="arabic"> +<li><p class="first">Copy <tt class="literal"><span class="pre">$ZSSL/z2s.py</span></tt> into <tt class="literal"><span class="pre">$ZOPE</span></tt>.</p> +</li> +<li><p class="first">Depending on your operating system, modify <tt class="literal"><span class="pre">$ZOPE/start</span></tt> or +<tt class="literal"><span class="pre">$ZOPE/start.bat</span></tt> to invoke <tt class="literal"><span class="pre">$ZOPE/z2s.py</span></tt>, instead of +<tt class="literal"><span class="pre">$ZOPE/z2.py</span></tt>. The files <tt class="literal"><span class="pre">$ZSSL/starts</span></tt> and +<tt class="literal"><span class="pre">$ZSSL/starts.bat</span></tt> serve as examples.</p> +</li> +<li><p class="first">Copy <tt class="literal"><span class="pre">$ZSSL/dh1024.pem</span></tt> into <tt class="literal"><span class="pre">$ZOPE</span></tt>. This file contains +Diffie-Hellman parameters for use by the SSL protocol.</p> +</li> +<li><p class="first">Copy <tt class="literal"><span class="pre">$ZSSL/randpool.dat</span></tt> into <tt class="literal"><span class="pre">$ZOPE</span></tt>. This file contains seed +material for the OpenSSL PRNG. Alternatively, create +<tt class="literal"><span class="pre">$ZOPE/randpool.dat</span></tt> thusly:</p> +<pre class="literal-block"> +$ dd if=/dev/urandom of=randpool.dat bs=1024 count=1 +</pre> +</li> +<li><p class="first">Copy <tt class="literal"><span class="pre">$ZSSL/ca.pem</span></tt> to <tt class="literal"><span class="pre">$ZOPE</span></tt>. This file contains an example +Certification Authority (CA) certificate. For information on +operating your own CA, see +<a class="reference" href="http://svn.osafoundation.org/m2crypto/trunk/doc/howto.ca.html">howto.ca.html</a> or one of numerous +similar documents available on the web.</p> +</li> +<li><p class="first">Copy <tt class="literal"><span class="pre">$ZSSL/server.pem</span></tt> to <tt class="literal"><span class="pre">$ZOPE</span></tt>. This file contains an RSA +key pair and its X.509v3 certificate issued by the above CA. You +may also create your own key/certificate bundle.</p> +</li> +<li><p class="first">Copy <tt class="literal"><span class="pre">$ZSSL/ZServer/HTTPS_Server.py</span></tt> to <tt class="literal"><span class="pre">$ZOPE/ZServer</span></tt>.</p> +</li> +<li><p class="first">Copy <tt class="literal"><span class="pre">$ZSSL/ZServer/__init__.py</span></tt> to <tt class="literal"><span class="pre">$ZOPE/ZServer</span></tt>. This +overwrites the existing <tt class="literal"><span class="pre">$ZOPE/ZServer/__init__.py</span></tt>. Alternatively, +apply the following patch to <tt class="literal"><span class="pre">$ZOPE/ZServer/__init__.py</span></tt>:</p> +<pre class="literal-block"> +--- __init__.py.org Sat Jun 21 23:20:41 2003 ++++ __init__.py Tue Jan 7 23:30:53 2003 +@@ -84,6 +84,7 @@ + import asyncore + from medusa import resolver, logger + from HTTPServer import zhttp_server, zhttp_handler ++from HTTPS_Server import zhttps_server, zhttps_handler + from PCGIServer import PCGIServer + from FCGIServer import FCGIServer + from FTPServer import FTPServer +</pre> +</li> +<li><p class="first">Copy <tt class="literal"><span class="pre">$ZSSL/ZServer/medusa/https_server.py</span></tt> to +<tt class="literal"><span class="pre">$ZOPE/ZServer/medusa</span></tt>.</p> +</li> +<li><p class="first">Stop Zope, if it is running.</p> +</li> +<li><p class="first">Start Zope with ZServerSSL thusly:</p> +<pre class="literal-block"> +./starts -X -f 9021 -w 9080 -W 9081 -y 9443 -Y 9444 +</pre> +<p>This starts the following:</p> +<ul class="simple"> +<li>an FTP server on port 9021</li> +<li>a HTTP server on port 9080</li> +<li>a WebDAV-source server on port 9081</li> +<li>a HTTPS server on port 9443</li> +<li>a WebDAV-source-over-HTTPS server on port 9444</li> +</ul> +</li> +</ol> +</div> +<div class="section" id="testing"> +<h1><a class="toc-backref" href="#id5" name="testing">Testing</a></h1> +<p>Below, we assume your Zope server is running on <tt class="literal"><span class="pre">localhost</span></tt>.</p> +<div class="section" id="https"> +<h2><a class="toc-backref" href="#id6" name="https">HTTPS</a></h2> +<p>This testing is done with Mozilla 1.1 on FreeBSD.</p> +<ol class="arabic simple"> +<li>With a browser, connect to <a class="reference" href="https://localhost:9443/">https://localhost:9443/</a>. Browse +around. Check out your browser's HTTPS informational screens.</li> +<li>Connect to <a class="reference" href="https://localhost:9443/manage">https://localhost:9443/manage</a>. Verify that you can +access Zope's management functionality.</li> +</ol> +</div> +<div class="section" id="webdav-over-https"> +<h2><a class="toc-backref" href="#id7" name="webdav-over-https">WebDAV-over-HTTPS</a></h2> +<p>This testing is done with Cadaver 0.21.0 on FreeBSD.</p> +<pre class="literal-block"> +$ cadaver https://localhost:9443/ +WARNING: Untrusted server certificate presented: +Issued to: M2Crypto, SG +Issued by: M2Crypto, SG +Do you wish to accept the certificate? (y/n) y +dav:/> ls +Listing collection `/': succeeded. +Coll: Channels 0 Jun 19 00:04 +Coll: Control_Panel 0 Jun 6 00:13 +Coll: Examples 0 Jun 6 00:12 +Coll: catalog 0 Jun 12 11:53 +Coll: ngps 0 Jun 16 15:34 +Coll: portal 0 Jun 21 15:21 +Coll: skunk 0 Jun 18 21:18 +Coll: temp_folder 0 Jun 22 17:57 +Coll: zope 0 Jun 20 15:27 + acl_users 0 Dec 30 1998 + browser_id_manager 0 Jun 6 00:12 + default.css 3037 Jun 21 16:38 + error_log 0 Jun 6 00:12 + index_html 313 Jun 12 13:36 + portal0 0 Jun 21 15:21 + session_data_manager 0 Jun 6 00:12 + standard_error_message 1365 Jan 21 2001 + standard_html_footer 50 Jun 12 12:30 + standard_html_header 80 Jan 21 2001 + standard_template.pt 282 Jun 6 00:12 + zsyncer 0 Jun 17 15:28 +dav:/> quit +Connection to `localhost' closed. +$ +</pre> +</div> +<div class="section" id="webdav-source-over-https"> +<h2><a class="toc-backref" href="#id8" name="webdav-source-over-https">WebDAV-Source-over-HTTPS</a></h2> +<p>This testing is done with Mozilla 1.1 on FreeBSD.</p> +<ol class="arabic simple"> +<li>Open the Mozilla Composer window.</li> +<li>Click "File", "Open Web Location". A dialog box appears.</li> +<li>Enter <tt class="literal"><span class="pre">https://localhost:9444/index_html</span></tt> for the URL.</li> +<li>Select "Open in new Composer window."</li> +<li>Click "Open". A new Composer window will open with <tt class="literal"><span class="pre">index_html</span></tt> +loaded.</li> +</ol> +</div> +<div class="section" id="python-with-m2crypto"> +<h2><a class="toc-backref" href="#id9" name="python-with-m2crypto">Python with M2Crypto</a></h2> +<p>This testing is done with M2Crypto 0.11 and Python 2.2.2 on FreeBSD.</p> +<div class="section" id="id1"> +<h3><a class="toc-backref" href="#id10" name="id1">HTTPS</a></h3> +<pre class="doctest-block"> +>>> from M2Crypto import Rand, SSL, m2urllib +>>> url = m2urllib.FancyURLopener() +>>> url.addheader('Connection', 'close') +>>> u = url.open('https://127.0.0.1:9443/') +send: 'GET / HTTP/1.1\r\nHost: 127.0.0.1:9443\r\nAccept-Encoding: identity\r\nUser-agent: Python-urllib/1.15\r\nConnection: close\r\n\r\n' +reply: 'HTTP/1.1 200 OK\r\n' +header: Server: ZServerSSL/0.11 +header: Date: Sun, 22 Jun 2003 13:42:34 GMT +header: Connection: close +header: Content-Type: text/html +header: Etag: +header: Content-Length: 535 +>>> while 1: +... data = u.read() +... if not data: break +... print data +... +</pre> +<pre class="literal-block"> +<html><head> +<base href="https://127.0.0.1:9443/" /> +<title>Zope</title></head><body bgcolor="#FFFFFF"> + +<h1>NgPS Desktop Portal</h1> + +&nbsp;&nbsp;So many hacks.<br> +&nbsp;&nbsp;So little time.<br> + +<h2>Link Farm</h2> +<ul> +<li><a href="http://localhost:8080/portal">Portal</a></li> +<li><a href="http://localhost/">Local Apache Home Page</a></li> +</ul> + +<hr><a href="http://www.zope.org/Credits" target="_top"><img src="https://127.0.0.1:9443/p_/ZopeButton" width="115" height="50" border="0" alt="Powered by Zope" /></a></body></html> +</pre> +<pre class="doctest-block"> +>>> u.close() +>>> +</pre> +</div> +<div class="section" id="xmlrpc-over-https"> +<h3><a class="toc-backref" href="#id11" name="xmlrpc-over-https">XMLRPC-over-HTTPS</a></h3> +<pre class="doctest-block"> +>>> from M2Crypto.m2xmlrpclib import Server, SSL_Transport +>>> zs = Server('https://127.0.0.1:9443/', SSL_Transport()) +>>> print zs.propertyMap() +[{'type': 'string', 'id': 'title', 'mode': 'w'}] +>>> +</pre> +</div> +</div> +</div> +<div class="section" id="conclusion"> +<h1><a class="toc-backref" href="#id12" name="conclusion">Conclusion</a></h1> +<p>Well, it works! ;-)</p> +</div> +</div> +</body> +</html> diff --git a/doc/howto.ca.html b/doc/howto.ca.html new file mode 100644 index 0000000..74142d5 --- /dev/null +++ b/doc/howto.ca.html @@ -0,0 +1,891 @@ +<HTML +><HEAD +><TITLE +>HOWTO: Creating your own CA with OpenSSL</TITLE +><META +NAME="GENERATOR" +CONTENT="Modular DocBook HTML Stylesheet Version 1.64 +"></HEAD +><BODY +CLASS="ARTICLE" +BGCOLOR="#FFFFFF" +TEXT="#000000" +LINK="#0000FF" +VLINK="#840084" +ALINK="#0000FF" +><DIV +CLASS="ARTICLE" +><DIV +CLASS="TITLEPAGE" +><H1 +CLASS="TITLE" +><A +NAME="AEN2" +>HOWTO: Creating your own CA with OpenSSL</A +></H1 +><H3 +CLASS="AUTHOR" +><A +NAME="AEN4" +>Pheng Siong Ng</A +></H3 +><DIV +CLASS="AFFILIATION" +><DIV +CLASS="ADDRESS" +><P +CLASS="ADDRESS" +>ngps@post1.com</P +></DIV +></DIV +><P +CLASS="COPYRIGHT" +>Copyright © 2000, 2001 by Ng Pheng Siong.</P +><DIV +CLASS="REVHISTORY" +><TABLE +WIDTH="100%" +BORDER="0" +><TR +><TH +ALIGN="LEFT" +VALIGN="TOP" +COLSPAN="3" +><B +>Revision History</B +></TH +></TR +><TR +><TD +ALIGN="LEFT" +>Revision $Revision: 1.1 $</TD +><TD +ALIGN="LEFT" +>$Date: 2003/06/22 16:41:18 $</TD +><TD +ALIGN="LEFT" +></TD +></TR +><TR +><TD +ALIGN="LEFT" +COLSPAN="3" +></TD +></TR +></TABLE +></DIV +><HR></DIV +><DIV +CLASS="SECT1" +><HR><H1 +CLASS="SECT1" +><A +NAME="INTRODUCTION" +>Introduction</A +></H1 +><P +>This is a HOWTO on creating your own <I +CLASS="EMPHASIS" +>certification + authority</I +> (<I +CLASS="EMPHASIS" +>CA</I +>) with OpenSSL. + </P +><P +>I last created a CA about a year ago, when I began work on <A +HREF="http://chandlerproject.org/Projects/MeTooCrypto" +TARGET="_top" +>M2Crypto</A +> and needed + certificates for the SSL bits. I accepted the tools' default settings + then, e.g., certificate validity of 365 days; this meant that my + certificates, including my CA's certificate, have now expired. + </P +><P +>Since I am using these certificates for M2Crypto's demonstration + programs (and I have forgotten the passphrase to the CA's private key), + I decided to discard the old CA and start afresh. I also decided to + document the process, hence this HOWTO. + </P +></DIV +><DIV +CLASS="SECT1" +><HR><H1 +CLASS="SECT1" +><A +NAME="PROCEDURE" +>The Procedure</A +></H1 +><P +>I use <TT +CLASS="FILENAME" +>CA.pl</TT +>, a Perl program written by + Steve Hanson and bundled with OpenSSL. + </P +><P +>The following are the steps to create a CA: + </P +><DIV +CLASS="PROCEDURE" +><OL +TYPE="1" +><LI +><P +>Choose a directory to do your CA work. All commands are executed + within this directory. Let's call the directory <TT +CLASS="FILENAME" +>demo</TT +>. + </P +></LI +><LI +><P +>Copy <TT +CLASS="FILENAME" +>CA.pl</TT +> and <TT +CLASS="FILENAME" +>openssl.cnf</TT +> + into <TT +CLASS="FILENAME" +>demo</TT +>. + </P +></LI +><LI +><P +>Apply the following patch to <TT +CLASS="FILENAME" +>CA.pl</TT +>, which + allows it to generate a CA certificate with a validity period of 1095 days, + i.e., 3 years: + </P +><PRE +CLASS="PROGRAMLISTING" +> --- CA.pl.org Sat Mar 31 12:40:13 2001 + +++ CA.pl Sat Mar 31 12:41:15 2001 + @@ -97,7 +97,7 @@ + } else { + print "Making CA certificate ...\n"; + system ("$REQ -new -x509 -keyout " . + - "${CATOP}/private/$CAKEY -out ${CATOP}/$CACERT $DAYS"); + + "${CATOP}/private/$CAKEY -out ${CATOP}/$CACERT -days 1095"); + $RET=$?; + } + } + </PRE +></LI +><LI +><P +>Create a new CA like this: + </P +><PRE +CLASS="SCREEN" +> <TT +CLASS="USERINPUT" +><B +>./CA.pl -newca + </B +></TT +> + A certificate filename (or enter to create) <TT +CLASS="USERINPUT" +><B +><TT +CLASS="REPLACEABLE" +><I +><enter></I +></TT +></B +></TT +> + + Making CA certificate ... + Using configuration from openssl.cnf + Generating a 1024 bit RSA private key + ............++++++ + ......................++++++ + writing new private key to './demoCA/private/cakey.pem' + Enter PEM pass phrase: <TT +CLASS="USERINPUT" +><B +><TT +CLASS="REPLACEABLE" +><I +><secret passphrase here></I +></TT +></B +></TT +> + Verifying password - Enter PEM pass phrase: <TT +CLASS="USERINPUT" +><B +><TT +CLASS="REPLACEABLE" +><I +><secret passphrase again></I +></TT +></B +></TT +> + ----- + You are about to be asked to enter information that will be incorporated + into your certificate request. + What you are about to enter is what is called a Distinguished Name or a DN. + There are quite a few fields but you can leave some blank + For some fields there will be a default value, + If you enter '.', the field will be left blank. + ----- + Country Name (2 letter code) [AU]:<TT +CLASS="USERINPUT" +><B +><TT +CLASS="REPLACEABLE" +><I +>SG</I +></TT +></B +></TT +> + State or Province Name (full name) [Some-State]:<TT +CLASS="USERINPUT" +><B +><TT +CLASS="REPLACEABLE" +><I +>.</I +></TT +></B +></TT +> + Locality Name (eg, city) []:<TT +CLASS="USERINPUT" +><B +><TT +CLASS="REPLACEABLE" +><I +>.</I +></TT +></B +></TT +>. + Organization Name (eg, company) [Internet Widgits Pty Ltd]:<TT +CLASS="USERINPUT" +><B +><TT +CLASS="REPLACEABLE" +><I +>DemoCA</I +></TT +></B +></TT +> + Organizational Unit Name (eg, section) []:<TT +CLASS="USERINPUT" +><B +><TT +CLASS="REPLACEABLE" +><I +>.</I +></TT +></B +></TT +> + Common Name (eg, YOUR name) []:<TT +CLASS="USERINPUT" +><B +><TT +CLASS="REPLACEABLE" +><I +>DemoCA Certificate Master</I +></TT +></B +></TT +> + Email Address []:<TT +CLASS="USERINPUT" +><B +><TT +CLASS="REPLACEABLE" +><I +>certmaster@democa.dom</I +></TT +></B +></TT +> + </PRE +><P +>This creates a new CA in the directory <TT +CLASS="FILENAME" +>demoCA</TT +>. + The CA's self-signed certificate is in + <TT +CLASS="FILENAME" +>demoCA/cacert.pem</TT +> and its RSA key pair is in + <TT +CLASS="FILENAME" +>demoCA/private/cakey.pem</TT +>. + </P +><P +><TT +CLASS="FILENAME" +>demoCA/private/cakey.pem</TT +> looks like this: + </P +><PRE +CLASS="SCREEN" +> <TT +CLASS="USERINPUT" +><B +>cat demoCA/private/cakey.pem + </B +></TT +> + -----BEGIN RSA PRIVATE KEY----- + Proc-Type: 4,ENCRYPTED + DEK-Info: DES-EDE3-CBC,19973A9DBBB601BA + + eOq9WFScNiI4/UWEUaSnGTKpJv2JYuMD3HwQox2Q3Cd4zGqVjJ6gF3exa5126cKf + X/bMVnwbPpuFZPiAIvaLyCjT6pYeXTBbSzs7/GQnvEOv+nYnDUFWi0Qm92qLk0uy + pFi/M1aWheN3vir2ZlAw+DW0bOOZhj8tC7Co7lMYb0YE271b6/YRPZCwQ3GXAHUJ + +aMYxlUDrK45aCUa/1CZDzTgk7h9cDgx2QJSIvYMYytCfI3zsuZMJS8/4OXLL0bI + lKmAc1dwB3DqGJt5XK4WJesiNfdxeCNEgAcYtEAgYZTPIApU+kTgTCIxJl2nMW7j + ax+Q1z7g+4MpgG20WD633D4z4dTlDdz+dnLi0rvuvxiwt+dUhrqiML1tyi+Z6EBH + jU4/cLBWev3rYfrlp4x8J9mDte0YKOk3t0wQOHqRetTsIfdtjnFp/Hu3qDmTCWjD + z/g7PPoO/bg/B877J9WBPbL/1hXXFYo88M+2aGlPOgDcFdiOqbLb2DCscohMbbVr + A4mgiy2kwWfIE73qiyV7yyG8FlRvr1iib+jbT3LTGf743utYAAs7HNGuOUObhoyt + jYvBD7ACn35P5YX7KTqvqErwdijxYCaNBCnvmRtmYSaNw9Kv1UJTxc5Vx7YLwIPk + E9KyBgKI7vPOjWBZ27+zOvNycmv1ciNtpALAw4bWtXnhCDVTHaVDy34OkheMzNCg + 2cjcBFzOkMIjcI03KbTQXOFIQGlsTWXGzkNf/zBQ+KksT1MCj+zBXSCvlDASMckg + kef21pGgUqPF14gKGfWX3sV4bjc1vbrRwq6zlG3nMuYqR5MtJJY9eQ== + -----END RSA PRIVATE KEY----- + </PRE +></LI +><LI +><P +>Next, generate a certificate request. + </P +><PRE +CLASS="SCREEN" +> <TT +CLASS="USERINPUT" +><B +>./CA.pl -newreq + </B +></TT +> + Using configuration from openssl.cnf + Generating a 1024 bit RSA private key + ..........++++++ + ..............++++++ + writing new private key to 'newreq.pem' + Enter PEM pass phrase: <TT +CLASS="USERINPUT" +><B +><TT +CLASS="REPLACEABLE" +><I +><another secret passphrase here></I +></TT +></B +></TT +> + Verifying password - Enter PEM pass phrase: <TT +CLASS="USERINPUT" +><B +><TT +CLASS="REPLACEABLE" +><I +><another secret passphrase again></I +></TT +></B +></TT +> + ----- + You are about to be asked to enter information that will be incorporated + into your certificate request. + What you are about to enter is what is called a Distinguished Name or a DN. + There are quite a few fields but you can leave some blank + For some fields there will be a default value, + If you enter '.', the field will be left blank. + ----- + Country Name (2 letter code) [AU]:<TT +CLASS="USERINPUT" +><B +><TT +CLASS="REPLACEABLE" +><I +>SG</I +></TT +></B +></TT +> + State or Province Name (full name) [Some-State]:.<TT +CLASS="USERINPUT" +><B +><TT +CLASS="REPLACEABLE" +><I +>.</I +></TT +></B +></TT +> + Locality Name (eg, city) []:<TT +CLASS="USERINPUT" +><B +><TT +CLASS="REPLACEABLE" +><I +>.</I +></TT +></B +></TT +> + Organization Name (eg, company) [Internet Widgits Pty Ltd]:<TT +CLASS="USERINPUT" +><B +><TT +CLASS="REPLACEABLE" +><I +>M2Crypto</I +></TT +></B +></TT +> + Organizational Unit Name (eg, section) []:<TT +CLASS="USERINPUT" +><B +><TT +CLASS="REPLACEABLE" +><I +>.</I +></TT +></B +></TT +> + Common Name (eg, YOUR name) []:<TT +CLASS="USERINPUT" +><B +><TT +CLASS="REPLACEABLE" +><I +>localhost</I +></TT +></B +></TT +> + Email Address []:<TT +CLASS="USERINPUT" +><B +><TT +CLASS="REPLACEABLE" +><I +>admin@server.example.dom</I +></TT +></B +></TT +> + + Please enter the following 'extra' attributes + to be sent with your certificate request + A challenge password []:<TT +CLASS="USERINPUT" +><B +><TT +CLASS="REPLACEABLE" +><I +><enter></I +></TT +></B +></TT +> + An optional company name []:<TT +CLASS="USERINPUT" +><B +><TT +CLASS="REPLACEABLE" +><I +><enter></I +></TT +></B +></TT +> + Request (and private key) is in newreq.pem + </PRE +><P +>The certificate request and private key in <TT +CLASS="FILENAME" +>newreq.pem</TT +> looks like this: + </P +><PRE +CLASS="SCREEN" +> <TT +CLASS="USERINPUT" +><B +>cat newreq.pem + </B +></TT +> + -----BEGIN RSA PRIVATE KEY----- + Proc-Type: 4,ENCRYPTED + DEK-Info: DES-EDE3-CBC,41B2874DF3D02DD4 + + mg611EoVkLEooSTv+qTM0Ddmm/M1jE/Jy5RD/sc3LSMhuGu9xc26OgsTJmkQuIAh + J/B4lAw8G59VTG6DykeEtrG0rUBx4bggc7PKbFuiN423YjJODWcHvVgnPOzXMQt+ + lY4tPl5+217MRHyx2NsWGrpkQNdu3GeSPOVMl3jeQiaXupONbwQ7rj42+X/VtAJP + W4D1NNwu8aGCPyShsEXHc/fI1WDpphYWke97pOjIZVQESFZOPty5HjIYZux4U+td + W81xODtq2ecJXc8fn2Wpa9y5VD1LT7oJksOuL1+Z04OVaeUe4x0swM17HlBm2kVt + fe/C/L6kN27MwZhE331VjtTjSGl4/gknqQDbLOtqT06f3OISsDJETm2itllyhgzv + C6Fi3N03rGFmKectijC+tws5k+P+HRG6sai33usk8xPokJqA+HYSWPz1XVlpRmv4 + kdjQOdST7ovU62mOTgf3ARcduPPwuzTfxOlYONe5NioO1APVHBrInQwcpLkpOTQR + vI4roIN+b75/nihUWGUJn/nbbBa2Yl0N5Gs1Tyiy9Z+CcRT2TfWKBBFlEUIFl7Mb + J9fTV3DI+k+akbR4il1NkQ8EcSmCr3WpA0I9n0EHI7ZVpVaHxc0sqaPFl8YGdFHq + 1Qk53C/w6+qPpDzT3yKFmG2LZytAAM1czvb6RbNRJJP2ZrpBwn/h99sUTo/yPfxY + nueYmFJDm0uVNtG0icXGNUfSfnjKNTtHPAgyKGetRIC3kgJz/bo2w7EI6iEjBAzK + l5TRm4x6ZJxwuXXMiJCehMMd8TC8ybwWO4AO19B3ebFFeTVsUgxSGA== + -----END RSA PRIVATE KEY----- + -----BEGIN CERTIFICATE REQUEST----- + MIIBnTCCAQYCAQAwXTELMAkGA1UEBhMCU0cxETAPBgNVBAoTCE0yQ3J5cHRvMRIw + EAYDVQQDEwlsb2NhbGhvc3QxJzAlBgkqhkiG9w0BCQEWGGFkbWluQHNlcnZlci5l + eGFtcGxlLmRvbTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAr1nYY1Qrll1r + uB/FqlCRrr5nvupdIN+3wF7q915tvEQoc74bnu6b8IbbGRMhzdzmvQ4SzFfVEAuM + MuTHeybPq5th7YDrTNizKKxOBnqE2KYuX9X22A1Kh49soJJFg6kPb9MUgiZBiMlv + tb7K3CHfgw5WagWnLl8Lb+ccvKZZl+8CAwEAAaAAMA0GCSqGSIb3DQEBBAUAA4GB + AHpoRp5YS55CZpy+wdigQEwjL/wSluvo+WjtpvP0YoBMJu4VMKeZi405R7o8oEwi + PdlrrliKNknFmHKIaCKTLRcU59ScA6ADEIWUzqmUzP5Cs6jrSRo3NKfg1bd09D1K + 9rsQkRc9Urv9mRBIsredGnYECNeRaK5R1yzpOowninXC + -----END CERTIFICATE REQUEST----- + </PRE +><P +>Decoding the certificate request gives the following: + </P +><PRE +CLASS="SCREEN" +> <TT +CLASS="USERINPUT" +><B +>openssl req -text -noout < newreq.pem + </B +></TT +> + Using configuration from /usr/local/pkg/openssl/openssl.cnf + Certificate Request: + Data: + Version: 0 (0x0) + Subject: C=SG, O=M2Crypto, CN=localhost/Email=admin@server.example.dom + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + RSA Public Key: (1024 bit) + Modulus (1024 bit): + 00:af:59:d8:63:54:2b:96:5d:6b:b8:1f:c5:aa:50: + 91:ae:be:67:be:ea:5d:20:df:b7:c0:5e:ea:f7:5e: + 6d:bc:44:28:73:be:1b:9e:ee:9b:f0:86:db:19:13: + 21:cd:dc:e6:bd:0e:12:cc:57:d5:10:0b:8c:32:e4: + c7:7b:26:cf:ab:9b:61:ed:80:eb:4c:d8:b3:28:ac: + 4e:06:7a:84:d8:a6:2e:5f:d5:f6:d8:0d:4a:87:8f: + 6c:a0:92:45:83:a9:0f:6f:d3:14:82:26:41:88:c9: + 6f:b5:be:ca:dc:21:df:83:0e:56:6a:05:a7:2e:5f: + 0b:6f:e7:1c:bc:a6:59:97:ef + Exponent: 65537 (0x10001) + Attributes: + a0:00 + Signature Algorithm: md5WithRSAEncryption + 7a:68:46:9e:58:4b:9e:42:66:9c:be:c1:d8:a0:40:4c:23:2f: + fc:12:96:eb:e8:f9:68:ed:a6:f3:f4:62:80:4c:26:ee:15:30: + a7:99:8b:8d:39:47:ba:3c:a0:4c:22:3d:d9:6b:ae:58:8a:36: + 49:c5:98:72:88:68:22:93:2d:17:14:e7:d4:9c:03:a0:03:10: + 85:94:ce:a9:94:cc:fe:42:b3:a8:eb:49:1a:37:34:a7:e0:d5: + b7:74:f4:3d:4a:f6:bb:10:91:17:3d:52:bb:fd:99:10:48:b2: + b7:9d:1a:76:04:08:d7:91:68:ae:51:d7:2c:e9:3a:8c:27:8a: + 75:c2 + </PRE +></LI +><LI +><P +>Now, sign the certificate request: + </P +><PRE +CLASS="SCREEN" +> <TT +CLASS="USERINPUT" +><B +>./CA.pl -sign + </B +></TT +> + Using configuration from openssl.cnf + Enter PEM pass phrase: <TT +CLASS="USERINPUT" +><B +><TT +CLASS="REPLACEABLE" +><I +><CA's passphrase></I +></TT +></B +></TT +> + Check that the request matches the signature + Signature ok + The Subjects Distinguished Name is as follows + countryName :PRINTABLE:'SG' + organizationName :PRINTABLE:'M2Crypto' + commonName :PRINTABLE:'localhost' + emailAddress :IA5STRING:'admin@server.example.dom' + Certificate is to be certified until Mar 31 02:57:30 2002 GMT (365 days) + Sign the certificate? [y/n]:<TT +CLASS="USERINPUT" +><B +><TT +CLASS="REPLACEABLE" +><I +>y</I +></TT +></B +></TT +> + + + 1 out of 1 certificate requests certified, commit? [y/n]<TT +CLASS="USERINPUT" +><B +><TT +CLASS="REPLACEABLE" +><I +>y</I +></TT +></B +></TT +> + Write out database with 1 new entries + Data Base Updated + Signed certificate is in newcert.pem + </PRE +><P +><TT +CLASS="FILENAME" +>newcert.pem</TT +> looks like this: + </P +><PRE +CLASS="SCREEN" +> <TT +CLASS="USERINPUT" +><B +>cat newcert.pem + </B +></TT +> + Certificate: + Data: + Version: 3 (0x2) + Serial Number: 1 (0x1) + Signature Algorithm: md5WithRSAEncryption + Issuer: C=SG, O=DemoCA, CN=DemoCA Certificate Master/Email=certmaster@democa.dom + Validity + Not Before: Mar 31 02:57:30 2001 GMT + Not After : Mar 31 02:57:30 2002 GMT + Subject: C=SG, O=M2Crypto, CN=localhost/Email=admin@server.example.dom + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + RSA Public Key: (1024 bit) + Modulus (1024 bit): + 00:af:59:d8:63:54:2b:96:5d:6b:b8:1f:c5:aa:50: + 91:ae:be:67:be:ea:5d:20:df:b7:c0:5e:ea:f7:5e: + 6d:bc:44:28:73:be:1b:9e:ee:9b:f0:86:db:19:13: + 21:cd:dc:e6:bd:0e:12:cc:57:d5:10:0b:8c:32:e4: + c7:7b:26:cf:ab:9b:61:ed:80:eb:4c:d8:b3:28:ac: + 4e:06:7a:84:d8:a6:2e:5f:d5:f6:d8:0d:4a:87:8f: + 6c:a0:92:45:83:a9:0f:6f:d3:14:82:26:41:88:c9: + 6f:b5:be:ca:dc:21:df:83:0e:56:6a:05:a7:2e:5f: + 0b:6f:e7:1c:bc:a6:59:97:ef + Exponent: 65537 (0x10001) + X509v3 extensions: + X509v3 Basic Constraints: + Certificate: + Data: + Version: 3 (0x2) + Serial Number: 1 (0x1) + Signature Algorithm: md5WithRSAEncryption + Issuer: C=SG, O=DemoCA, CN=DemoCA Certificate Master/Email=certmaster@democa.dom + Validity + Not Before: Mar 31 02:57:30 2001 GMT + Not After : Mar 31 02:57:30 2002 GMT + Subject: C=SG, O=M2Crypto, CN=localhost/Email=admin@server.example.dom + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + RSA Public Key: (1024 bit) + Modulus (1024 bit): + 00:af:59:d8:63:54:2b:96:5d:6b:b8:1f:c5:aa:50: + 91:ae:be:67:be:ea:5d:20:df:b7:c0:5e:ea:f7:5e: + 6d:bc:44:28:73:be:1b:9e:ee:9b:f0:86:db:19:13: + 21:cd:dc:e6:bd:0e:12:cc:57:d5:10:0b:8c:32:e4: + c7:7b:26:cf:ab:9b:61:ed:80:eb:4c:d8:b3:28:ac: + 4e:06:7a:84:d8:a6:2e:5f:d5:f6:d8:0d:4a:87:8f: + 6c:a0:92:45:83:a9:0f:6f:d3:14:82:26:41:88:c9: + 6f:b5:be:ca:dc:21:df:83:0e:56:6a:05:a7:2e:5f: + 0b:6f:e7:1c:bc:a6:59:97:ef + Exponent: 65537 (0x10001) + X509v3 extensions: + X509v3 Basic Constraints: + CA:FALSE + Netscape Comment: + OpenSSL Generated Certificate + X509v3 Subject Key Identifier: + B3:D6:89:88:2F:B1:15:40:EC:0A:C0:30:35:3A:B7:DA:72:73:1B:4D + X509v3 Authority Key Identifier: + keyid:F9:6A:A6:34:97:6B:BC:BB:5A:17:0D:19:FC:62:21:0B:00:B5:0E:29 + DirName:/C=SG/O=DemoCA/CN=DemoCA Certificate Master/Email=certmaster@democa.dom + serial:00 + + Signature Algorithm: md5WithRSAEncryption + </PRE +></LI +><LI +><P +>In certain situations, e.g., where your certificate and + private key are to be used in an unattended SSL server, you may wish to + not encrypt the private key, i.e., leave the key in the clear. This + decision should be governed by your site's security policy and threat + model, of course. + </P +><PRE +CLASS="SCREEN" +> <TT +CLASS="USERINPUT" +><B +>openssl rsa < newkey.pem > newkey2.pem + </B +></TT +> + read RSA key + Enter PEM pass phrase:<TT +CLASS="USERINPUT" +><B +><TT +CLASS="REPLACEABLE" +><I +><secret passphrase here></I +></TT +></B +></TT +> + writing RSA key + </PRE +><P +><TT +CLASS="FILENAME" +>newkey2.pem</TT +> looks like this: + </P +><PRE +CLASS="SCREEN" +> <TT +CLASS="USERINPUT" +><B +>cat newkey2.pem + </B +></TT +> + -----BEGIN RSA PRIVATE KEY----- + MIICXgIBAAKBgQCvWdhjVCuWXWu4H8WqUJGuvme+6l0g37fAXur3Xm28RChzvhue + 7pvwhtsZEyHN3Oa9DhLMV9UQC4wy5Md7Js+rm2HtgOtM2LMorE4GeoTYpi5f1fbY + DUqHj2ygkkWDqQ9v0xSCJkGIyW+1vsrcId+DDlZqBacuXwtv5xy8plmX7wIDAQAB + AoGAbAkU8w3W1Qu15Hle1bJSL7GMReoreqeblOBmMAZz4by0l6sXZXJpjWXo86f/ + +dASMYTMPC4ZTYtv06N07AFbjL+kDfqDMTfzQkYMHp1LAq1Ihbq1rHWSBH5n3ekq + KiY8JKpv8DR5Po1iKaXJFuDByGDENJwYbSRSpSK3P+vkWWECQQDkEUE/ZPqqqZkQ + 2iWRPAsCbEID8SAraQl3DdCLYs/GgARfmmj4yUHEwkys9Jo1H8k4BdxugmaUwNi5 + YQ/CVzrXAkEAxNO80ArbGxPUmr11GHG/bGBYj1DUBkHZSc7dgxZdtUCLGNxQnNsg + Iwq3n6j1sUzS3UW6abQ8bivYNOUcMKJAqQJBANQxFaLU4b/NQaODQ3aoBZpAfP9L + 5eFdvbet+7zjt2r5CpikgkwOfAmDuXEltx/8LevY0CllW+nErx9zJgVrwUsCQQCu + 76H5JiznPBDSF2FjgHWqVVdgyW4owY3mU739LHvNBLicN/RN9VPy0Suy8/CqzKT9 + lWPBXzf2k3FuUdNkRlFBAkEAmpXoybuiFR2S5Bma/ax96lVs0/VihhfC1zZP/X/F + Br77+h9dIul+2DnyOl50zu0Sdzst1/7ay4JSDHyiBCMGSQ== + -----END RSA PRIVATE KEY----- + </PRE +></LI +></OL +></DIV +><P +>That's it! The certificate, <TT +CLASS="FILENAME" +>newcert.pem</TT +>, and + the private key - <TT +CLASS="FILENAME" +>newkey.pem</TT +> (encrypted) or + <TT +CLASS="FILENAME" +>newkey2.pem</TT +> (unencrypted) - are now ready to be used. + You may wish to rename the files to more intuitive names. + </P +><P +>You should also keep the CA's certificate <TT +CLASS="FILENAME" +>demo/cacert.pem + </TT +> handy for use when developing and deploying SSL or S/MIME + applications. + </P +></DIV +><DIV +CLASS="SECT1" +><HR><H1 +CLASS="SECT1" +><A +NAME="CONCLUSION" +>Conclusion</A +></H1 +><P +>We've walked through the basic steps in the creation of a CA and + certificates using the tools that come with OpenSSL. We did not cover more + advanced topics such as constraining a certificate to be SSL-only or + S/MIME-only. + </P +><P +>There exist several HOWTOs similar to this one on the net. This one + is written specifically to facilitate discussions in my other HOWTOs + on developing SSL and S/MIME applications in + <A +HREF="http://www.python.org" +TARGET="_top" +>Python</A +> using + <A +HREF="http://chandlerproject.org/Projects/MeTooCrypto" +TARGET="_top" +>M2Crypto</A +>. + </P +></DIV +><DIV +CLASS="SECT1" +><HR><H1 +CLASS="SECT1" +><A +NAME="ID-KLUDGE" +></A +></H1 +><P +> <TT +CLASS="LITERAL" +>$Id:howto.ca.html 583 2007-10-01 19:23:12Z heikki $</TT +> + </P +></DIV +></DIV +></BODY +></HTML +>
\ No newline at end of file diff --git a/doc/howto.smime.html b/doc/howto.smime.html new file mode 100644 index 0000000..21bc5cb --- /dev/null +++ b/doc/howto.smime.html @@ -0,0 +1,1570 @@ +<HTML +><HEAD +><TITLE +>HOWTO: Programming S/MIME in Python with M2Crypto</TITLE +><META +NAME="GENERATOR" +CONTENT="Modular DocBook HTML Stylesheet Version 1.64 +"></HEAD +><BODY +CLASS="ARTICLE" +BGCOLOR="#FFFFFF" +TEXT="#000000" +LINK="#0000FF" +VLINK="#840084" +ALINK="#0000FF" +><DIV +CLASS="ARTICLE" +><DIV +CLASS="TITLEPAGE" +><H1 +CLASS="TITLE" +><A +NAME="AEN2" +>HOWTO: Programming S/MIME in Python with M2Crypto</A +></H1 +><H3 +CLASS="AUTHOR" +><A +NAME="AEN4" +>Pheng Siong Ng</A +></H3 +><DIV +CLASS="AFFILIATION" +><DIV +CLASS="ADDRESS" +><P +CLASS="ADDRESS" +>ngps@post1.com</P +></DIV +></DIV +><P +CLASS="COPYRIGHT" +>Copyright © 2000, 2001 by Ng Pheng Siong.</P +><DIV +CLASS="REVHISTORY" +><TABLE +WIDTH="100%" +BORDER="0" +><TR +><TH +ALIGN="LEFT" +VALIGN="TOP" +COLSPAN="3" +><B +>Revision History</B +></TH +></TR +><TR +><TD +ALIGN="LEFT" +>Revision $Id: howto.smime.html 646 2008-10-29 03:12:56Z heikki $</TD +><TD +ALIGN="LEFT" +></TD +><TD +ALIGN="LEFT" +></TD +></TR +><TR +><TD +ALIGN="LEFT" +COLSPAN="3" +></TD +></TR +></TABLE +></DIV +><HR></DIV +><DIV +CLASS="SECT1" +><HR><H1 +CLASS="SECT1" +><A +NAME="INTRODUCTION" +>Introduction</A +></H1 +><P +><A +HREF="http://chandlerproject.org/Projects/MeTooCrypto" +TARGET="_top" +>M2Crypto</A +> + is a <A +HREF="http://www.python.org" +TARGET="_top" +>Python</A +> + interface to <A +HREF="http://www.openssl.org" +TARGET="_top" +>OpenSSL</A +>. It makes + available to the Python programmer SSL functionality to implement clients + and servers, S/MIME v2, RSA, DSA, DH, symmetric ciphers, message digests and + HMACs. + </P +><P +>This document demonstrates programming S/MIME with M2Crypto. + </P +></DIV +><DIV +CLASS="SECT1" +><HR><H1 +CLASS="SECT1" +><A +NAME="SMIME" +>S/MIME</A +></H1 +><P +>S/MIME - Secure Multipurpose Internet Mail Extensions + [<SPAN +CLASS="CITATION" +>RFC 2311, RFC 2312</SPAN +>] - provides a + consistent way to send and receive secure MIME data. Based on the popular + Internet MIME standard, S/MIME provides the following cryptographic security + services for electronic messaging applications - + <I +CLASS="EMPHASIS" +>authentication</I +>, <I +CLASS="EMPHASIS" +>message integrity </I +> + and <I +CLASS="EMPHASIS" +>non-repudiation of origin </I +> (using <I +CLASS="EMPHASIS" +>digital + signatures</I +>), and <I +CLASS="EMPHASIS" +>privacy </I +> and <I +CLASS="EMPHASIS" +>data + security</I +> (using <I +CLASS="EMPHASIS" +>encryption</I +>). + </P +></DIV +><DIV +CLASS="SECT1" +><HR><H1 +CLASS="SECT1" +><A +NAME="KEYS-AND-CERTIFICATES" +>Keys and Certificates</A +></H1 +><P +>To create an S/MIME-signed message, you need an RSA key pair + (this consists of a public key and a private key) and an X.509 + certificate of said public key. + </P +><P +>To create an S/MIME-encrypted message, you need an X.509 + certificate for each recipient. + </P +><P +>To create an S/MIME-signed <I +CLASS="EMPHASIS" +>and</I +> -encrypted + message, first create a signed message, then encrypt the signed + message with the recipients' certificates. + </P +><P +>You may generate key pairs and obtain certificates by using a + commercial <I +CLASS="EMPHASIS" +>certification authority</I +> service. + </P +><P +>You can also do so using freely-available software. For many + purposes, e.g., automated S/MIME messaging by system administration + processes, this approach is cheap and effective. + </P +><P +>We now work through using OpenSSL to generate key pairs and + certificates. This assumes you have OpenSSL installed properly on your + system. + </P +><P +>First, we generate an X.509 certificate to be used for signing: + </P +><PRE +CLASS="SCREEN" +> <TT +CLASS="USERINPUT" +><B +>openssl req -newkey rsa:1024 -nodes -x509 -days 365 -out signer.pem + </B +></TT +> + Using configuration from /usr/local/pkg/openssl/openssl.cnf + Generating a 1024 bit RSA private key + ..++++++ + ....................++++++ + writing new private key to 'privkey.pem' + ----- + You are about to be asked to enter information that will be incorporated + into your certificate request. + What you are about to enter is what is called a Distinguished Name or a DN. + There are quite a few fields but you can leave some blank + For some fields there will be a default value, + If you enter '.', the field will be left blank. + ----- + Country Name (2 letter code) [AU]:<TT +CLASS="USERINPUT" +><B +><TT +CLASS="REPLACEABLE" +><I +>SG</I +></TT +></B +></TT +> + State or Province Name (full name) [Some-State]:<TT +CLASS="USERINPUT" +><B +><TT +CLASS="REPLACEABLE" +><I +>.</I +></TT +></B +></TT +> + Locality Name (eg, city) []:<TT +CLASS="USERINPUT" +><B +><TT +CLASS="REPLACEABLE" +><I +>.</I +></TT +></B +></TT +> + Organization Name (eg, company) [Internet Widgits Pty Ltd]:<TT +CLASS="USERINPUT" +><B +><TT +CLASS="REPLACEABLE" +><I +>M2Crypto</I +></TT +></B +></TT +> + Organizational Unit Name (eg, section) []:<TT +CLASS="USERINPUT" +><B +><TT +CLASS="REPLACEABLE" +><I +>.</I +></TT +></B +></TT +> + Common Name (eg, YOUR name) []:<TT +CLASS="USERINPUT" +><B +><TT +CLASS="REPLACEABLE" +><I +>S/MIME Sender</I +></TT +></B +></TT +> + Email Address []:<TT +CLASS="USERINPUT" +><B +><TT +CLASS="REPLACEABLE" +><I +>sender@example.dom</I +></TT +></B +></TT +> + </PRE +><P +>This generates a 1024-bit RSA key pair, unencrypted, into + <TT +CLASS="FILENAME" +>privkey.pem</TT +>; it also generates a self-signed X.509 + certificate for the public key into <TT +CLASS="FILENAME" +>signer.pem</TT +>. The + certificate is valid for 365 days, i.e., a year. + </P +><P +>Let's rename <TT +CLASS="FILENAME" +>privkey.pem</TT +> so that we know it is + a companion of <TT +CLASS="FILENAME" +>signer.pem</TT +>'s: + </P +><PRE +CLASS="SCREEN" +> <TT +CLASS="USERINPUT" +><B +>mv privkey.pem signer_key.pem</B +></TT +> + </PRE +><P +>To verify the content of <TT +CLASS="FILENAME" +>signer.pem</TT +>, execute the + following: + </P +><PRE +CLASS="SCREEN" +> <TT +CLASS="USERINPUT" +><B +>openssl x509 -noout -text -in signer.pem + </B +></TT +> + Certificate: + Data: + Version: 3 (0x2) + Serial Number: 0 (0x0) + Signature Algorithm: md5WithRSAEncryption + Issuer: C=SG, O=M2Crypto, CN=S/MIME Sender/Email=sender@example.dom + Validity + Not Before: Mar 24 12:56:16 2001 GMT + Not After : Mar 24 12:56:16 2002 GMT + Subject: C=SG, O=M2Crypto, CN=S/MIME Sender/Email=sender@example.dom + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + RSA Public Key: (1024 bit) + Modulus (1024 bit): + 00:a9:d6:e2:b5:11:3b:ae:3c:e2:17:31:70:e1:6e: + 01:f4:19:6d:bd:2a:42:36:2b:37:34:e2:83:1d:0d: + 11:2e:b4:99:44:db:10:67:be:97:5f:5b:1a:26:33: + 46:23:2f:95:04:7a:35:da:9d:f9:26:88:39:9e:17: + cd:3e:eb:a8:19:8d:a8:2a:f1:43:da:55:a9:2e:2c: + 65:ed:04:71:42:ce:73:53:b8:ea:7e:c7:f0:23:c6: + 63:c5:5e:68:96:64:a7:b4:2a:94:26:76:eb:79:ea: + e3:4e:aa:82:09:4f:44:87:4a:12:62:b5:d7:1f:ca: + f2:ce:d5:ba:7e:1f:48:fd:b9 + Exponent: 65537 (0x10001) + X509v3 extensions: + X509v3 Subject Key Identifier: + 29:FB:38:B6:BF:E2:40:BB:FF:D5:71:D7:D5:C4:F0:83:1A:2B:C7:99 + X509v3 Authority Key Identifier: + keyid:29:FB:38:B6:BF:E2:40:BB:FF:D5:71:D7:D5:C4:F0:83:1A:2B:C7:99 + DirName:/C=SG/O=M2Crypto/CN=S/MIME Sender/Email=sender@example.dom + serial:00 + + X509v3 Basic Constraints: + CA:TRUE + Signature Algorithm: md5WithRSAEncryption + 68:c8:6b:1b:fa:7c:9a:39:35:76:18:15:c9:fd:89:97:62:db: + 7a:b0:2d:13:dd:97:e8:1b:7a:9f:22:27:83:24:9d:2e:56:ec: + 97:89:3c:ef:16:55:80:5a:18:7c:22:d0:f6:bb:e3:a4:e8:59: + 30:ff:99:5a:93:3e:ea:bc:ee:7f:8d:d6:7d:37:8c:ac:3d:74: + 80:ce:7a:99:ba:27:b9:2a:a3:71:fa:a5:25:ba:47:17:df:07: + 56:96:36:fd:60:b9:6c:96:06:e8:e3:7b:9f:4b:6a:95:71:a8: + 34:fc:fc:b5:88:8b:c4:3f:1e:24:f6:52:47:b2:7d:44:67:d9: + 83:e8 + </PRE +><P +>Next, we generate a self-signed X.509 certificate for the + recipient. Note that <TT +CLASS="FILENAME" +>privkey.pem</TT +> will be + recreated. + </P +><PRE +CLASS="SCREEN" +> <TT +CLASS="USERINPUT" +><B +>openssl req -newkey rsa:1024 -nodes -x509 -days 365 -out recipient.pem + </B +></TT +> + Using configuration from /usr/local/pkg/openssl/openssl.cnf + Generating a 1024 bit RSA private key + .....................................++++++ + .................++++++ + writing new private key to 'privkey.pem' + ----- + You are about to be asked to enter information that will be incorporated + into your certificate request. + What you are about to enter is what is called a Distinguished Name or a DN. + There are quite a few fields but you can leave some blank + For some fields there will be a default value, + If you enter '.', the field will be left blank. + ----- + Country Name (2 letter code) [AU]:<TT +CLASS="USERINPUT" +><B +><TT +CLASS="REPLACEABLE" +><I +>SG</I +></TT +></B +></TT +> + State or Province Name (full name) [Some-State]:<TT +CLASS="USERINPUT" +><B +><TT +CLASS="REPLACEABLE" +><I +>.</I +></TT +></B +></TT +> + Locality Name (eg, city) []:<TT +CLASS="USERINPUT" +><B +><TT +CLASS="REPLACEABLE" +><I +>.</I +></TT +></B +></TT +> + Organization Name (eg, company) [Internet Widgits Pty Ltd]:<TT +CLASS="USERINPUT" +><B +><TT +CLASS="REPLACEABLE" +><I +>M2Crypto</I +></TT +></B +></TT +> + Organizational Unit Name (eg, section) []:<TT +CLASS="USERINPUT" +><B +><TT +CLASS="REPLACEABLE" +><I +>.</I +></TT +></B +></TT +> + Common Name (eg, YOUR name) []:<TT +CLASS="USERINPUT" +><B +><TT +CLASS="REPLACEABLE" +><I +>S/MIME Recipient</I +></TT +></B +></TT +> + Email Address []:<TT +CLASS="USERINPUT" +><B +><TT +CLASS="REPLACEABLE" +><I +>recipient@example.dom</I +></TT +></B +></TT +> + </PRE +><P +>Again, rename <TT +CLASS="FILENAME" +>privkey.pem</TT +>: + </P +><PRE +CLASS="SCREEN" +> <TT +CLASS="USERINPUT" +><B +>mv privkey.pem recipient_key.pem</B +></TT +> + </PRE +><P +>In the examples to follow, S/MIME Sender, + <TT +CLASS="EMAIL" +><<A +HREF="mailto:sender@example.dom" +>sender@example.dom</A +>></TT +>, shall be the sender of S/MIME messages, + while S/MIME Recipient, <TT +CLASS="EMAIL" +><<A +HREF="mailto:recipient@example.dom" +>recipient@example.dom</A +>></TT +>, shall be the + recipient of S/MIME messages. + </P +><P +>Armed with the key pairs and certificates, we are now ready to begin + programming S/MIME in Python. + </P +><DIV +CLASS="NOTE" +><BLOCKQUOTE +CLASS="NOTE" +><P +><B +>Note: </B +>The private keys generated above are + <I +CLASS="EMPHASIS" +>not passphrase-protected</I +>, i.e., they are + <I +CLASS="EMPHASIS" +>in the clear</I +>. Anyone who has access to such a key can + generate S/MIME-signed messages with it, and decrypt S/MIME messages + encrypted to it's corresponding public key. + </P +><P +>We may passphrase-protect the keys, if we so choose. M2Crypto will + prompt the user for the passphrase when such a key is being loaded. + </P +></BLOCKQUOTE +></DIV +></DIV +><DIV +CLASS="SECT1" +><HR><H1 +CLASS="SECT1" +><A +NAME="M2CRYPTO-SMIME" +>M2Crypto.SMIME</A +></H1 +><P +>The Python programmer accesses M2Crypto's S/MIME functionality + through class <TT +CLASS="CLASSNAME" +>SMIME</TT +> in the module + <TT +CLASS="CLASSNAME" +>M2Crypto.SMIME</TT +>. Typically, an <TT +CLASS="CLASSNAME" +>SMIME</TT +> + object is instantiated; the object is then set up for the intended + operation: sign, encrypt, decrypt or verify; finally, the operation is invoked on + the object. + </P +><P +><TT +CLASS="CLASSNAME" +>M2Crypto.SMIME</TT +> makes extensive use of + <TT +CLASS="CLASSNAME" +>M2Crypto.BIO</TT +>: <TT +CLASS="CLASSNAME" +>M2Crypto.BIO</TT +> + is a Python abstraction of the <TT +CLASS="CLASSNAME" +>BIO</TT +> abstraction in + OpenSSL. A commonly used <TT +CLASS="CLASSNAME" +>BIO</TT +> abstraction in M2Crypto is + <TT +CLASS="CLASSNAME" +>M2Crypto.BIO.MemoryBuffer</TT +>, which implements a + memory-based file-like object, similar to Python's own + <TT +CLASS="CLASSNAME" +>StringIO</TT +>. + </P +></DIV +><DIV +CLASS="SECT1" +><HR><H1 +CLASS="SECT1" +><A +NAME="SIGN" +>Sign</A +></H1 +><P +>The following code demonstrates how to generate an S/MIME-signed + message. <TT +CLASS="FILENAME" +>randpool.dat</TT +> contains random data which is + used to seed OpenSSL's pseudo-random number generator via M2Crypto. + </P +><PRE +CLASS="PROGRAMLISTING" +> from M2Crypto import BIO, Rand, SMIME + + def makebuf(text): + return BIO.MemoryBuffer(text) + + # Make a MemoryBuffer of the message. + buf = makebuf('a sign of our times') + + # Seed the PRNG. + Rand.load_file('randpool.dat', -1) + + # Instantiate an SMIME object; set it up; sign the buffer. + s = SMIME.SMIME() + s.load_key('signer_key.pem', 'signer.pem') + p7 = s.sign(buf) + </PRE +><P +><TT +CLASS="VARNAME" +>p7</TT +> now contains a <I +CLASS="EMPHASIS" +>PKCS #7 signature + blob</I +> wrapped in an <TT +CLASS="CLASSNAME" +>M2Crypto.SMIME.PKCS7</TT +> + object. Note that <TT +CLASS="VARNAME" +>buf</TT +> has been consumed by + <TT +CLASS="FUNCTION" +>sign()</TT +> and has to be recreated if it is to be used + again. + </P +><P +>We may now send the signed message via SMTP. In these examples, we + shall not do so; instead, we'll render the S/MIME output in + mail-friendly format, and pretend that our messages are sent and + received correctly. + </P +><PRE +CLASS="PROGRAMLISTING" +> # Recreate buf. + buf = makebuf('a sign of our times') + + # Output p7 in mail-friendly format. + out = BIO.MemoryBuffer() + out.write('From: sender@example.dom\n') + out.write('To: recipient@example.dom\n') + out.write('Subject: M2Crypto S/MIME testing\n') + s.write(out, p7, buf) + + print out.read() + + # Save the PRNG's state. + Rand.save_file('randpool.dat') + </PRE +><P +>Here's the output: + </P +><PRE +CLASS="SCREEN" +> From: sender@example.dom + To: recipient@example.dom + Subject: M2Crypto S/MIME testing + MIME-Version: 1.0 + Content-Type: multipart/signed ; protocol="application/x-pkcs7-signature" ; micalg=sha1 ; boundary="----3C93156FC7B4EBF49FE9C7DB7F503087" + + This is an S/MIME signed message + + ------3C93156FC7B4EBF49FE9C7DB7F503087 + a sign of our times + ------3C93156FC7B4EBF49FE9C7DB7F503087 + Content-Type: application/x-pkcs7-signature; name="smime.p7s" + Content-Transfer-Encoding: base64 + Content-Disposition: attachment; filename="smime.p7s" + + MIIE8AYJKoZIhvcNAQcCoIIE4TCCBN0CAQExCzAJBgUrDgMCGgUAMCIGCSqGSIb3 + DQEHAaAVBBNhIHNpZ24gb2Ygb3VyIHRpbWVzoIIC5zCCAuMwggJMoAMCAQICAQAw + DQYJKoZIhvcNAQEEBQAwWzELMAkGA1UEBhMCU0cxETAPBgNVBAoTCE0yQ3J5cHRv + MRYwFAYDVQQDEw1TL01JTUUgU2VuZGVyMSEwHwYJKoZIhvcNAQkBFhJzZW5kZXJA + ZXhhbXBsZS5kb20wHhcNMDEwMzMxMTE0MDMzWhcNMDIwMzMxMTE0MDMzWjBbMQsw + CQYDVQQGEwJTRzERMA8GA1UEChMITTJDcnlwdG8xFjAUBgNVBAMTDVMvTUlNRSBT + ZW5kZXIxITAfBgkqhkiG9w0BCQEWEnNlbmRlckBleGFtcGxlLmRvbTCBnzANBgkq + hkiG9w0BAQEFAAOBjQAwgYkCgYEA5c5Tj1CHTSOxa1q2q0FYiwMWYHptJpJcvtZm + UwrgU5sHrA8OnCM0cDXEj0KPf3cfNjHffB8HWMzI4UEgNmFXQNsxoGZ+iqwxLlNj + y9Mh7eFW/Bjq5hNXbouSlQ0rWBRkoxV64y+t6lQehb32WfYXQbKFxFJSXzSxOx3R + 8YhSPd0CAwEAAaOBtjCBszAdBgNVHQ4EFgQUXOyolL1t4jaBwZFRM7MS8nBLzUow + gYMGA1UdIwR8MHqAFFzsqJS9beI2gcGRUTOzEvJwS81KoV+kXTBbMQswCQYDVQQG + EwJTRzERMA8GA1UEChMITTJDcnlwdG8xFjAUBgNVBAMTDVMvTUlNRSBTZW5kZXIx + ITAfBgkqhkiG9w0BCQEWEnNlbmRlckBleGFtcGxlLmRvbYIBADAMBgNVHRMEBTAD + AQH/MA0GCSqGSIb3DQEBBAUAA4GBAHo3DrCHR86fSTVAvfiXdSswWqKtCEhUHRdC + TLFGl4hDk2GyZxaFuqZwiURz/H7nMicymI2wkz8H/wyHFg8G3BIehURpj2v/ZWXY + eovbgS7EZALVVkDj4hNl/IIHWd6Gtv1UODf7URbxtl3hQ9/eTWITrefT1heuPnar + 8czydsOLMYIBujCCAbYCAQEwYDBbMQswCQYDVQQGEwJTRzERMA8GA1UEChMITTJD + cnlwdG8xFjAUBgNVBAMTDVMvTUlNRSBTZW5kZXIxITAfBgkqhkiG9w0BCQEWEnNl + bmRlckBleGFtcGxlLmRvbQIBADAJBgUrDgMCGgUAoIGxMBgGCSqGSIb3DQEJAzEL + BgkqhkiG9w0BBwEwHAYJKoZIhvcNAQkFMQ8XDTAxMDMzMTExNDUwMlowIwYJKoZI + hvcNAQkEMRYEFOoeRUd8ExIYXfQq8BTFuKWrSP3iMFIGCSqGSIb3DQEJDzFFMEMw + CgYIKoZIhvcNAwcwDgYIKoZIhvcNAwICAgCAMA0GCCqGSIb3DQMCAgFAMAcGBSsO + AwIHMA0GCCqGSIb3DQMCAgEoMA0GCSqGSIb3DQEBAQUABIGAQpU8hFUtLCF6hO2t + ec9EYJ/Imqqiiw+BxWxkUUVT81Vbjwdn9JST6+sztM5JRP2ZW+b4txEjZriYC8f3 + kv95YMTGbIsuWkJ93GrbvqoJ/CxO23r9WWRnZEm/1EZN9ZmlrYqzBTxnNRmP3Dhj + cW8kzZwH+2/2zz2G7x1HxRWH95A= + + ------3C93156FC7B4EBF49FE9C7DB7F503087-- + </PRE +></DIV +><DIV +CLASS="SECT1" +><HR><H1 +CLASS="SECT1" +><A +NAME="VERIFY" +>Verify</A +></H1 +><P +>Assume the above output has been saved into <TT +CLASS="FILENAME" +>sign.p7</TT +>. + Let's now verify the signature: + </P +><PRE +CLASS="PROGRAMLISTING" +> from M2Crypto import SMIME, X509 + + # Instantiate an SMIME object. + s = SMIME.SMIME() + + # Load the signer's cert. + x509 = X509.load_cert('signer.pem') + sk = X509.X509_Stack() + sk.push(x509) + s.set_x509_stack(sk) + + # Load the signer's CA cert. In this case, because the signer's + # cert is self-signed, it is the signer's cert itself. + st = X509.X509_Store() + st.load_info('signer.pem') + s.set_x509_store(st) + + # Load the data, verify it. + p7, data = SMIME.smime_load_pkcs7('sign.p7') + v = s.verify(p7) + print v + print data + print data.read() + </PRE +><P +>Here's the output of the above program: + </P +><PRE +CLASS="SCREEN" +> a sign of our times + <M2Crypto.BIO.BIO instance at 0x822012c> + a sign of our times + </PRE +><P +>Suppose, instead of loading <TT +CLASS="FILENAME" +>signer.pem</TT +> above, + we load <TT +CLASS="FILENAME" +>recipient.pem</TT +>. That is, we do a global + substitution of <TT +CLASS="LITERAL" +>recipient.pem</TT +> for + <TT +CLASS="LITERAL" +>signer.pem</TT +> in the above program. Here's the modified + program's output: + </P +><PRE +CLASS="SCREEN" +> Traceback (most recent call last): + File "./verify.py", line 22, in ? + v = s.verify(p7) + File "/usr/local/home/ngps/prog/m2/M2Crypto/SMIME.py", line 205, in verify + raise SMIME_Error, Err.get_error() + M2Crypto.SMIME.SMIME_Error: 312:error:21075075:PKCS7 routines:PKCS7_verify:certificate verify error:pk7_smime.c:213:Verify error:self signed certificate + </PRE +><P +>As displayed, the error is generated by line 213 of OpenSSL's + <TT +CLASS="FILENAME" +>pk7_smime.c</TT +> (as of OpenSSL 0.9.6); if you are a + C programmer, you may wish to look up the C source to explore OpenSSL's + S/MIME implementation and understand why the error message is worded thus. + </P +></DIV +><DIV +CLASS="SECT1" +><HR><H1 +CLASS="SECT1" +><A +NAME="ENCRYPT" +>Encrypt</A +></H1 +><P +>We now demonstrate how to generate an S/MIME-encrypted message: + </P +><PRE +CLASS="PROGRAMLISTING" +> from M2Crypto import BIO, Rand, SMIME, X509 + + def makebuf(text): + return BIO.MemoryBuffer(text) + + # Make a MemoryBuffer of the message. + buf = makebuf('a sign of our times') + + # Seed the PRNG. + Rand.load_file('randpool.dat', -1) + + # Instantiate an SMIME object. + s = SMIME.SMIME() + + # Load target cert to encrypt to. + x509 = X509.load_cert('recipient.pem') + sk = X509.X509_Stack() + sk.push(x509) + s.set_x509_stack(sk) + + # Set cipher: 3-key triple-DES in CBC mode. + s.set_cipher(SMIME.Cipher('des_ede3_cbc')) + + # Encrypt the buffer. + p7 = s.encrypt(buf) + + # Output p7 in mail-friendly format. + out = BIO.MemoryBuffer() + out.write('From: sender@example.dom\n') + out.write('To: recipient@example.dom\n') + out.write('Subject: M2Crypto S/MIME testing\n') + s.write(out, p7) + + print out.read() + + # Save the PRNG's state. + Rand.save_file('randpool.dat') + </PRE +><P +>Here's the output of the above program: + </P +><PRE +CLASS="SCREEN" +> From: sender@example.dom + To: recipient@example.dom + Subject: M2Crypto S/MIME testing + MIME-Version: 1.0 + Content-Disposition: attachment; filename="smime.p7m" + Content-Type: application/x-pkcs7-mime; name="smime.p7m" + Content-Transfer-Encoding: base64 + + MIIBVwYJKoZIhvcNAQcDoIIBSDCCAUQCAQAxggEAMIH9AgEAMGYwYTELMAkGA1UE + BhMCU0cxETAPBgNVBAoTCE0yQ3J5cHRvMRkwFwYDVQQDExBTL01JTUUgUmVjaXBp + ZW50MSQwIgYJKoZIhvcNAQkBFhVyZWNpcGllbnRAZXhhbXBsZS5kb20CAQAwDQYJ + KoZIhvcNAQEBBQAEgYCBaXZ+qjpBEZwdP7gjfzfAtQitESyMwo3i+LBOw6sSDir6 + FlNDPCnkrTvqDX3Rt6X6vBtTCYOm+qiN7ujPkOU61cN7h8dvHR8YW9+0IPY80/W0 + lZ/HihSRgwTNd7LnxUUcPx8YV1id0dlmP0Hz+Lg+mHf6rqaR//JcYhX9vW4XvjA7 + BgkqhkiG9w0BBwEwFAYIKoZIhvcNAwcECMN+qya6ADywgBgHr9Jkhwn5Gsdu7BwX + nIQfYTYcdL9I5Sk= + </PRE +></DIV +><DIV +CLASS="SECT1" +><HR><H1 +CLASS="SECT1" +><A +NAME="DECRYPT" +>Decrypt</A +></H1 +><P +>Assume the above output has been saved into <TT +CLASS="FILENAME" +>encrypt.p7</TT +>. + Decrypt the message thusly: + </P +><PRE +CLASS="PROGRAMLISTING" +> from M2Crypto import BIO, SMIME, X509 + + # Instantiate an SMIME object. + s = SMIME.SMIME() + + # Load private key and cert. + s.load_key('recipient_key.pem', 'recipient.pem') + + # Load the encrypted data. + p7, data = SMIME.smime_load_pkcs7('encrypt.p7') + + # Decrypt p7. + out = s.decrypt(p7) + + print out + </PRE +><P +>Here's the output: + </P +><PRE +CLASS="SCREEN" +> a sign of our times + </PRE +></DIV +><DIV +CLASS="SECT1" +><HR><H1 +CLASS="SECT1" +><A +NAME="SIGN-AND-ENCRYPT" +>Sign and Encrypt</A +></H1 +><P +>Here's how to generate an S/MIME-signed/encrypted message: + </P +><PRE +CLASS="PROGRAMLISTING" +> from M2Crypto import BIO, Rand, SMIME, X509 + + def makebuf(text): + return BIO.MemoryBuffer(text) + + # Make a MemoryBuffer of the message. + buf = makebuf('a sign of our times') + + # Seed the PRNG. + Rand.load_file('randpool.dat', -1) + + # Instantiate an SMIME object. + s = SMIME.SMIME() + + # Load signer's key and cert. Sign the buffer. + s.load_key('signer_key.pem', 'signer.pem') + p7 = s.sign(buf) + + # Load target cert to encrypt the signed message to. + x509 = X509.load_cert('recipient.pem') + sk = X509.X509_Stack() + sk.push(x509) + s.set_x509_stack(sk) + + # Set cipher: 3-key triple-DES in CBC mode. + s.set_cipher(SMIME.Cipher('des_ede3_cbc')) + + # Create a temporary buffer. + tmp = BIO.MemoryBuffer() + + # Write the signed message into the temporary buffer. + s.write(tmp, p7, buf) + + # Encrypt the temporary buffer. + p7 = s.encrypt(tmp) + + # Output p7 in mail-friendly format. + out = BIO.MemoryBuffer() + out.write('From: sender@example.dom\n') + out.write('To: recipient@example.dom\n') + out.write('Subject: M2Crypto S/MIME testing\n') + s.write(out, p7) + + print out.read() + + # Save the PRNG's state. + Rand.save_file('randpool.dat') + </PRE +><P +>Here's the output of the above program: + </P +><PRE +CLASS="SCREEN" +> From: sender@example.dom + To: recipient@example.dom + Subject: M2Crypto S/MIME testing + MIME-Version: 1.0 + Content-Disposition: attachment; filename="smime.p7m" + Content-Type: application/x-pkcs7-mime; name="smime.p7m" + Content-Transfer-Encoding: base64 + + MIIIwwYJKoZIhvcNAQcDoIIItDCCCLACAQAxggEAMIH9AgEAMGYwYTELMAkGA1UE + BhMCU0cxETAPBgNVBAoTCE0yQ3J5cHRvMRkwFwYDVQQDExBTL01JTUUgUmVjaXBp + ZW50MSQwIgYJKoZIhvcNAQkBFhVyZWNpcGllbnRAZXhhbXBsZS5kb20CAQAwDQYJ + KoZIhvcNAQEBBQAEgYBlZlGupFphwhsGtIAPvDExN61qisz3oem88xoXkUW0SzoR + B9zJFFAuQTWzdNJgrKKYikhWjDojaAc/PFl1K5dYxRgtZLB36ULJD/v/yWmxnjz8 + TvtK+Wbal2P/MH2pZ4LVERXa/snTElhCawUlwtiFz/JvY5CiF/dcwd+AwFQq4jCC + B6UGCSqGSIb3DQEHATAUBggqhkiG9w0DBwQIRF525UfwszaAggeA85RmX6AXQMxb + eBDz/LJeCgc3RqU1UwIsbKMquIs1S46Ebbm5nP75izPnujOkJ2hv+LNzqOWADmOl + +CnGEq1qxTyduIgUDA2nBgCL/gVyVy+/XC9dtImUUTxtxLgYtB0ujkBNsOaENOlM + fv4SGM3jkR+K/xlYG6HHzZGbfYyNGj2Y7yMZ1rL1m8SnRNmkCysKGTrudeNf6wT9 + J6wO9DzLTioz3ZnVr3LjsSKIb4tIp4ugqNJaLuW7m3FtZ3MAgxN68hBbJs8TZ8tL + V/0jwUqS+grcgZEb9ymfcedxahtDUfHjRkpDpsxZzVVGkSBNcbQu92oByQVnRQ8m + wrYLp3/eawM5AvuV7HNpTT5ZR+1t8luishHN9899IMP2Vyg0Ub67FqFypYmM2cm2 + sjAI4KpfvT00XFNvgLuYwYEKs9syGTO7hiHNQKcF44F5LYv6nTFwmFQB11dAtY9V + ull4D2CLDx9OvyNyKwdEZB5dyV0r/uKIdkhST60V2Q9KegpzgFpoZtSKM/HPYSVH + 1Bc9f3Q/GqZCvNZZCMx8UvRjQR8dRWDSmPJ0VXG1+wJ+fCmSPP3AuQ1/VsgPRqx2 + 56VrpGPpGut40hV8xQFbWIZ2whwWLKPFAHj8B79ZtFUzUrU6Z2rNpvv8inHc/+S/ + b6GR5s8/gucRblvd7n3OFNX5UJmPmcw9zWbu/1Dr9DY8l0nAQh21y5FGSS8B1wdE + oD2M3Lp7JbwjQbRtnDhImqul2S4yu+m+wDD1aR2K4k3GAI7KKgOBWT0+BDClcn8A + 4Ju6/YUbj33YlMPJgnGijLnolFy0hNW7TmWqR+8tSI3wO5eNKg4qwBnarqc3vgCV + quVxINAXyGQCO9lzdw6hudk8/+BlweGdqhONaIWbK5z1L/SfQo6LC9MTsj7FJydq + bc+kEbfZS8aSq7uc9axW6Ti0eAPJ8EVHtwhSBgZQRweKFBXs6HbbhMIdc4N0M7Oq + UiFXaF6s4n2uihVP6TqXtHEjTpZoC7pC+HCYiuKXUJtaqtXBOh+y3KLvHk09YL6D + XmTDg+UTiFsh4jKKm/BhdelbR5JbpJcj5AId76Mfr8+F/1g9ePOvsWHpQr/oIQTo + xEkaxCmzEgP0b6caMWfMUQrbVGxBBNcqKc/ir9fGGOPHATzzq/xLcQYvK1tZhd/D + ah/gpMPndsyvVCEuFPluWyDiM0VkwHgC2/3pJIYFHaxK64IutmPsy393rHMEB4kN + AHau6kWK+yL9qEVH1pP2zvswQ12P7gjt3T/G3bGsmvlXkEfztfjkXo6XnjcBNf5y + G+974AKLcjnk1gzIgarz+lAMY57Gkw4oNDMrTqVQ2OJQlvOSbllPXzH+aAiavB8W + ZPECLLwHxD4B1AuaiAArgKl935u/TOB+yQOR8JgGsUzROyJqHJ/SC51HkebgCkL1 + aggtjgPlIBEXLZAlhpWLZ9lAQyrQpvCVJYwaOvfMmvRav4NAFNoZ2/Q7S4Tn1z+U + XX+f+GD58P4MPMhU5IKnz4yH4nlHnAiTEvcs85TZUAXze9g/uBOwZITeGtyLi52S + aETIr4v7SgXMepX7ThQ1Pv/jddsK/u4j2F34u0XktwCP+UrbfkE2mocdXvdzxbmd + tZSznK2qwgVSsPOs9MhUaepbnjmNBFFBrULhrUtSglM/VX/rWNiyh0aw4XYyHhIt + 9ZNlfEjKjJ67VEMBxBJ/ieUCouRGCxPYD1j65VT7oB3ZiyPu2F2nlUIcYNqPg1Sd + QBCrdaOXdJ0uLwyTAUeVE+wMbgscLvWsfZcCCJHAvw9NHFMUcnrdWxAYMVETNUOn + uryVAK7VfOldaz6z3NOSOi6nonNeHpR/sipBa4ik5xCRLT9e0S2QJgRvO9GyfAqz + 3DIzHtxIGePFzTiUYUTxS3i2gnMX2PEe3ChTLlYWD3jNeAKz0iOzpDphIF2xHLLQ + 1tCAqBmq/vUzALyDFFdFuTIqQZys4z/u4Dmyq9uXs421eN3v2hkVHvDy8uT2Ot29 + lg4Q5YezR1EjaW//9guL1BXbcKrTEdtxeNqtem7SpZOMTSwD2lhB8z65GrX90Cyt + EMmaRSGYEdf5h1afL1SmKOMskbqxe1D2jG/vsXC7XX7xO/ioy0BdiJcYN1JiMOHJ + EOzFol5I20YkiV6j+cenfQFwc/NkaSxEkR8AUHJSbvUmRQRl6r0nnsFpZdR1w7pv + wkaT+eOpZynO4mY/ZtF6MpXJsixi6L4ZYXEbS6yHf+XGFfB0okILylmwv2bf6+Mq + nqXlmGj3Jwq7X9/+2BDqvfpFFX5lSmItKZAobLdssjFR6roJxOqRsGia2aZ+0+U5 + VhgdITtnElgtHBaeZU5rHDswgdeLVBP+rGWnKxpJ+pLtNNi25sPYRcWFL6Erd25u + eXiY8GEIr+u7rqBWpc9HR34sAPRs3ubbCUleT748keCbx247ImBtiDctZxcc1O86 + +0QjHP6HUT7FSo/FmT7a120S3Gd2jixGh06l/9ij5Z6mJa7Rm7TTbSjup/XISnOT + MKWcbI1nfVOhCv3xDq2eLae+s0oVoc041ceRazqFM2TL/Z6UXRME + </PRE +></DIV +><DIV +CLASS="SECT1" +><HR><H1 +CLASS="SECT1" +><A +NAME="DECRYPT-AND-VERIFY" +>Decrypt and Verify</A +></H1 +><P +>Suppose the above output has been saved into + <TT +CLASS="FILENAME" +>se.p7</TT +>. The following demonstrates how to decrypt and + verify it: + </P +><PRE +CLASS="PROGRAMLISTING" +> from M2Crypto import BIO, SMIME, X509 + + # Instantiate an SMIME object. + s = SMIME.SMIME() + + # Load private key and cert. + s.load_key('recipient_key.pem', 'recipient.pem') + + # Load the signed/encrypted data. + p7, data = SMIME.smime_load_pkcs7('se.p7') + + # After the above step, 'data' == None. + # Decrypt p7. 'out' now contains a PKCS #7 signed blob. + out = s.decrypt(p7) + + # Load the signer's cert. + x509 = X509.load_cert('signer.pem') + sk = X509.X509_Stack() + sk.push(x509) + s.set_x509_stack(sk) + + # Load the signer's CA cert. In this case, because the signer's + # cert is self-signed, it is the signer's cert itself. + st = X509.X509_Store() + st.load_info('signer.pem') + s.set_x509_store(st) + + # Recall 'out' contains a PKCS #7 blob. + # Transform 'out'; verify the resulting PKCS #7 blob. + p7_bio = BIO.MemoryBuffer(out) + p7, data = SMIME.smime_load_pkcs7_bio(p7_bio) + v = s.verify(p7) + + print v + </PRE +><P +>The output is as follows: + </P +><PRE +CLASS="SCREEN" +> a sign of our times + </PRE +></DIV +><DIV +CLASS="SECT1" +><HR><H1 +CLASS="SECT1" +><A +NAME="SMTP" +>Sending S/MIME messages via SMTP</A +></H1 +><P +>In the above examples, we've assumed that our S/MIME messages + are sent and received automagically. The following is a Python function that + generates S/MIME-signed/encrypted messages and sends them via SMTP: + </P +><PRE +CLASS="PROGRAMLISTING" +> from M2Crypto import BIO, SMIME, X509 + import smtplib, string, sys + + def sendsmime(from_addr, to_addrs, subject, msg, from_key, from_cert=None, to_certs=None, smtpd='localhost'): + + msg_bio = BIO.MemoryBuffer(msg) + sign = from_key + encrypt = to_certs + + s = SMIME.SMIME() + if sign: + s.load_key(from_key, from_cert) + p7 = s.sign(msg_bio, flags=SMIME.PKCS7_TEXT) + msg_bio = BIO.MemoryBuffer(msg) # Recreate coz sign() has consumed it. + + if encrypt: + sk = X509.X509_Stack() + for x in to_certs: + sk.push(X509.load_cert(x)) + s.set_x509_stack(sk) + s.set_cipher(SMIME.Cipher('des_ede3_cbc')) + tmp_bio = BIO.MemoryBuffer() + if sign: + s.write(tmp_bio, p7) + else: + tmp_bio.write(msg) + p7 = s.encrypt(tmp_bio) + + out = BIO.MemoryBuffer() + out.write('From: %s\r\n' % from_addr) + out.write('To: %s\r\n' % string.join(to_addrs, ", ")) + out.write('Subject: %s\r\n' % subject) + if encrypt: + s.write(out, p7) + else: + if sign: + s.write(out, p7, msg_bio, SMIME.PKCS7_TEXT) + else: + out.write('\r\n') + out.write(msg) + out.close() + + smtp = smtplib.SMTP() + smtp.connect(smtpd) + smtp.sendmail(from_addr, to_addrs, out.read()) + smtp.quit() + </PRE +><P +>This function sends plain, S/MIME-signed, S/MIME-encrypted, + and S/MIME-signed/encrypted messages, depending on the parameters + <TT +CLASS="PARAMETER" +><I +>from_key</I +></TT +> and <TT +CLASS="PARAMETER" +><I +>to_certs</I +></TT +>. The + function's output interoperates with Netscape Messenger. + </P +></DIV +><DIV +CLASS="SECT1" +><HR><H1 +CLASS="SECT1" +><A +NAME="VERIFYING-ORIGIN" +>Verifying origin of S/MIME messages</A +></H1 +><P +>In our examples above that decrypt or verify messages, we skipped + a step: verifying that the <TT +CLASS="LITERAL" +>from</TT +> address of the message + matches the <TT +CLASS="LITERAL" +>email address</TT +> attribute in the sender's + certificate. + </P +><P +>The premise of current X.509 certification practice is that the + CA is supposed to verify your identity, and to issue a certificate with + <TT +CLASS="LITERAL" +>email address</TT +> that matches your actual mail address. + (Verisign's March 2001 failure in identity verification resulting in + Microsoft certificates being issued to spoofers notwithstanding.) + </P +><P +>If you run your own CA, your certification practice is up to you, of + course, and it would probably be part of your security policy. + </P +><P +>Whether your S/MIME messaging application needs to verify the + <TT +CLASS="LITERAL" +>from</TT +> addresses of S/MIME messages depends on your + security policy and your system's threat model, as always. + </P +></DIV +><DIV +CLASS="SECT1" +><HR><H1 +CLASS="SECT1" +><A +NAME="NETSCAPE-MESSENGER" +>Interoperating with Netscape Messenger</A +></H1 +><P +>Suppose S/MIME Recipient uses Netscape Messenger. To enable Messenger + to handle S/MIME messages from S/MIME Sender, S/MIME Recipient needs + to configure Messenger with his private key and certificate, as well as S/MIME + Sender's certificate. + </P +><DIV +CLASS="NOTE" +><BLOCKQUOTE +CLASS="NOTE" +><P +><B +>Note: </B +>Configuring Messenger's POP or IMAP settings so that it retrieves + mail correctly is beyond the scope of this HOWTO. + </P +></BLOCKQUOTE +></DIV +><P +>The following steps demonstrate how to import S/MIME Recipient's + private key and certificate for Messenger: + </P +><DIV +CLASS="PROCEDURE" +><OL +TYPE="1" +><LI +><P +>Transform S/MIME Recipient's private key and certificate into + <I +CLASS="EMPHASIS" +>PKCS #12</I +> format. + </P +><PRE +CLASS="SCREEN" +> <TT +CLASS="USERINPUT" +><B +>openssl pkcs12 -export -in recipient.pem -inkey recipient_key.pem -name "S/MIME Recipient" -out recipient.p12 + </B +></TT +> + Enter Export Password:<TT +CLASS="USERINPUT" +><B +><TT +CLASS="REPLACEABLE" +><I +><enter></I +></TT +></B +></TT +> + Verifying password - Enter Export Password:<TT +CLASS="USERINPUT" +><B +><TT +CLASS="REPLACEABLE" +><I +><enter></I +></TT +></B +></TT +> + </PRE +></LI +><LI +><P +>Start Messenger. + </P +></LI +><LI +><P +>Click on the (open) "lock" icon at the bottom left corner of + Messenger's window. This brings up the "Security Info" dialog box. + </P +></LI +><LI +><P +>Click on "Yours" under "Certificates". + </P +></LI +><LI +><P +>Select "Import a certificate", then pick + <TT +CLASS="FILENAME" +>recipient.p12</TT +> from the ensuing file selection dialog + box. + </P +></LI +></OL +></DIV +><P +>Next, you need to import <TT +CLASS="FILENAME" +>signer.pem</TT +> as a CA + certificate, so that Messenger will mark messages signed by S/MIME Sender as + "trusted": + </P +><DIV +CLASS="PROCEDURE" +><OL +TYPE="1" +><LI +><P +>Create a DER encoding of <TT +CLASS="FILENAME" +>signer.pem</TT +>. + </P +><PRE +CLASS="SCREEN" +> <TT +CLASS="USERINPUT" +><B +>openssl x509 -inform pem -outform der -in signer.pem -out signer.der + </B +></TT +> + </PRE +></LI +><LI +><P +>Install <TT +CLASS="FILENAME" +>signer.der</TT +> into Messenger as MIME type + <TT +CLASS="LITERAL" +>application/x-x509-ca-cert</TT +>. You do this by downloading + <TT +CLASS="FILENAME" +>signer.der</TT +> via Navigator from a HTTP or HTTPS server, + with the correct MIME type mapping. (You may use + <TT +CLASS="FILENAME" +>demo/ssl/https_srv.py</TT +>, bundled with M2Crypto, for + this purpose.) Follow the series of dialog boxes to accept + <TT +CLASS="FILENAME" +>signer.der</TT +> as a CA for certifying email users. + </P +></LI +></OL +></DIV +><P +>S/MIME Recipient is now able to decrypt and read S/MIME Sender's + messages with Messenger. Messenger will indicate that S/MIME Sender's + messages are signed, encrypted, or encrypted <I +CLASS="EMPHASIS" +>and</I +> + signed, as the case may be, via the "stamp" icon on the message window's + top right corner. + </P +><P +>Clicking on the "stamp" icon brings you to the Security Info dialog + box. Messenger informs you that the message is, say, encrypted with + 168-bit DES-EDE3-CBC and that it is digitally signed by the private key + corresponding to the public key contained in the certificate + <TT +CLASS="FILENAME" +>signer.pem</TT +>. + </P +></DIV +><DIV +CLASS="SECT1" +><HR><H1 +CLASS="SECT1" +><A +NAME="MICROSOFT-OUTLOOK" +>Interoperating with Microsoft Outlook</A +></H1 +><P +>I do not know how to do this, as I do not use Outlook. (Nor do I use + Netscape Messenger, actually. I use Mutt, top dog of MUAs. ;-) Information on + how to configure Outlook with keys and certificates so that it handles + S/MIME mail is gratefully accepted. + </P +></DIV +><DIV +CLASS="SECT1" +><HR><H1 +CLASS="SECT1" +><A +NAME="ZSMIME" +>ZSmime</A +></H1 +><P +>ZSmime is a <A +HREF="http://www.zope.org" +TARGET="_top" +>Zope</A +> + <I +CLASS="EMPHASIS" +>product</I +> that enables Zope to generate + S/MIME-signed/encrypted messages. ZSmime demonstrates how to invoke + M2Crypto in a web application server extension. + </P +><P +>ZSmime has its own <A +HREF="http://sandbox.rulemaker.net/ngps/zope/zsmime/howto.html" +TARGET="_top" +>HOWTO</A +> + explaining its usage. (That HOWTO has some overlap in content with + this document.) + </P +></DIV +><DIV +CLASS="SECT1" +><HR><H1 +CLASS="SECT1" +><A +NAME="RESOURCES" +>Resources</A +></H1 +><P +></P +><UL +><LI +STYLE="list-style-type: opencircle" +><P +>IETF S/MIME Working Group - + <A +HREF="http://www.imc.org/ietf-smime" +TARGET="_top" +> http://www.imc.org/ietf-smime</A +> + </P +></LI +><LI +STYLE="list-style-type: opencircle" +><P +>S/MIME and OpenPGP - + <A +HREF="http://www.imc.org/smime-pgpmime.html" +TARGET="_top" +> http://www.imc.org/smime-pgpmime.html</A +> + </P +></LI +><LI +STYLE="list-style-type: opencircle" +><P +>S/MIME Freeware Library - + <A +HREF="http://www.getronicsgov.com/hot/sfl_home.htm" +TARGET="_top" +> http://www.getronicsgov.com/hot/sfl_home.htm</A +> + </P +></LI +><LI +STYLE="list-style-type: opencircle" +><P +>Mozilla Network Security Services - + <A +HREF="http://www.mozilla.org/projects/security/pkg/nss" +TARGET="_top" +> http://www.mozilla.org/projects/security/pkg/nss</A +> + </P +></LI +><LI +STYLE="list-style-type: opencircle" +><P +>S/MIME Cracking Screen Saver - + <A +HREF="http://www.counterpane.com/smime.html" +TARGET="_top" +> http://www.counterpane.com/smime.html</A +> + </P +></LI +></UL +></DIV +><DIV +CLASS="SECT1" +><HR><H1 +CLASS="SECT1" +><A +NAME="ID-KLUDGE" +></A +></H1 +><P +> <TT +CLASS="LITERAL" +>$Id:howto.smime.html 583 2007-10-01 19:23:12Z heikki $</TT +> + </P +></DIV +></DIV +></BODY +></HTML +>
\ No newline at end of file diff --git a/doc/howto.ssl.html b/doc/howto.ssl.html new file mode 100644 index 0000000..340f264 --- /dev/null +++ b/doc/howto.ssl.html @@ -0,0 +1,206 @@ +<HTML +><HEAD +><TITLE +>HOWTO: Programming SSL in Python with M2Crypto</TITLE +> + +</HEAD> +<BODY +CLASS="ARTICLE" +BGCOLOR="#FFFFFF" +TEXT="#000000" +LINK="#0000FF" +VLINK="#840084" +ALINK="#0000FF" +> + +<DIV +CLASS="TITLEPAGE" +><H1 +CLASS="TITLE" +><A +NAME="AEN2" +>HOWTO: Programming SSL in Python with M2Crypto</A +></H1 +> +<P> +Ng Pheng Siong (ngps@netmemetic.com) and Heikki Toivonen (heikki@osafoundation.org) +</P +><P +CLASS="COPYRIGHT" +>Copyright © 2001, 2002 by Ng Pheng Siong.</P +> +<P +CLASS="COPYRIGHT" +>Portions Copyright © 2006 by Open Source Applications Foundation.</P +> +</DIV> + +<DIV +CLASS="SECT1" +><H1 +CLASS="SECT1" +><A +NAME="INTRODUCTION" +>Introduction</A +></H1 +><P +><A +HREF="http://chandlerproject.org/Projects/MeTooCrypto" +TARGET="_top" +>M2Crypto</A +> + is a <A +HREF="http://www.python.org" +TARGET="_top" +>Python</A +> + interface to <A +HREF="http://www.openssl.org" +TARGET="_top" +>OpenSSL</A +>. It makes + available to the Python programmer SSL functionality to implement clients + and servers, S/MIME v2, RSA, DSA, DH, symmetric ciphers, message digests and + HMACs. + </P +><P +>This document demonstrates programming HTTPS with M2Crypto. + </P +></DIV +> + + +<DIV +CLASS="SECT1" +><H1 +CLASS="SECT1" +><A +NAME="history" +>A bit of history</A +></H1 +> + +<p> M2Crypto was created during the time of Python 1.5, which features + a module httplib providing client-side HTTP functionality. M2Crypto sports + a httpslib based on httplib. + </p> + + <p> + Beginning with version 2.0, Python's socket module provided + (rudimentary) SSL support. Also in the same version, httplib was + enhanced with class HTTPConnection, which is more sophisticated than + the old class HTTP, and HTTPSConnection, which does HTTPS. + </p> + + <p> + Subsequently, M2Crypto.httpslib grew a compatible (but not identical) + class HTTPSConnection. + </p> + + <p> + The primary interface difference between the two HTTPSConnection + classes is that M2Crypto's version accepts an M2Crypto.SSL.Context + instance as a parameter, whereas Python 2.x's SSL support does not + permit Pythonic control of the SSL context. + </p> + + <p> Within the implementations, Python's + <tt>HTTPSConnection</tt> employs a + <tt>FakeSocket</tt> object, which collects all input from + the SSL connection before returning it to the application as a + <tt>StringIO</tt> buffer, whereas M2Crypto's + <tt>HTTPSConnection</tt> uses a buffering + <tt>M2Crypto.BIO.IOBuffer</tt> object that works over the + underlying M2Crypto.SSL.Connection directly. </p> + + <p>Since then M2Crypto has gained a Twisted wrapper that allows securing + Twisted SSL connections with M2Crypto.</p> +</DIV +> + + +<DIV CLASS="SECT1" id="secure" name="secure"> +<H1 CLASS="SECT1">Secure SSL</H1> + +<p>It is recommended that you read the book Network Security with OpenSSL by John Viega, Matt Messier and Pravir Chandra, +ISBN 059600270X.</p> + +<p>Using M2Crypto does not automatically make an SSL connection secure. There are various steps that need to be made +before we can make that claim. Let's see how a simple client can establish a secure connection:</p> + +<pre> +ctx = SSL.Context() +ctx.set_verify(SSL.verify_peer | SSL.verify_fail_if_no_peer_cert, depth=9) +if ctx.load_verify_locations('ca.pem') != 1: raise Exception('No CA certs') +s = SSL.Connection(ctx) +s.connect(server_address) +# Normal protocol (for example HTTP) commands follow +</pre> + +<p>The first line creates an SSL context. The defaults allow any SSL version (except SSL version 2 which has known +weaknesses) and sets the allowed ciphers to secure ones.</p> + +<p>The second line tells M2Crypto to perform certificate validation. The flags shown above are typical for clients, +and requires the server to send a certificate. The depth parameter tells how long certificate chains are allowed - +9 is pretty common default, although probably too long in practice.</p> + +<p>The third line loads the allowed root (certificate authority or CA) certificates. +Most Linux distributions come with CA certificates in suitable format. You +could also download the <a href="http://mxr.mozilla.org/seamonkey/source//security/nss/lib/ckfw/builtins/certdata.txt?raw=1">certdata.txt</a> +file from the <a href="http://www.mozilla.org/projects/security/pki/nss/">NSS</a> +project and convert it +with the little M2Crypto utility script <a href="http://svn.osafoundation.org/m2crypto/trunk/demo/x509/certdata2pem.py">demo/x509/certdata2pem.py</a>.</p> + +<p>The fourth line creates an SSL connection object with the secure context.</p> + +<p>The fifth line connects to the server. During this time we perform the last security step: just after connection, but before +exchanging any data, we compare the commonName (or subjectAltName DNS field) field in the certificate the server returned to the +server address we tried to connect to. This happens automatically with SSL.Connection and the Twisted wrapper class, and anything +that uses those. In all other cases you must do the check manually. It is recommended you call the SSL.Checker to do the actual check.</p> + +<p>SSL servers are different in that they typically do not require the client to send a certificate, so there is usually no certificate +checking. Also, it is typically useless to perform host name checking.</p> + +</DIV> + +<DIV CLASS="SECT1"> +<H1 CLASS="SECT1">Code Samples</H1> + +<p>The best samples of how to use the various SSL objects are in the tests directory, and the test_ssl.py file specifically. +There are additional samples in the demo directory, but they are not quaranteed to be up to date.</p> + +<p>NOTE: The tests and demos +may not be secure as is. Use the information above on how to make them secure.</p> +</DIV> + +<DIV +CLASS="SECT1" +><H1 +CLASS="SECT1" +><A +NAME="SSLDUMP" +>ssldump</A +></H1 +> +<P>ssldump "is an SSLv3/TLS network protocol analyser. It identifies + TCP connections on the chosen network interface and attempts to interpret + them as SSLv3/TLS traffic. When it identifies SSLv3/TLS traffic, it + decodes the records and displays them in a textual form to stdout. If + provided with the appropriate keying material, it will also decrypt the + connections and display the application data traffic. + </P> + + <P> + If linked with OpenSSL, ssldump can display certificates in decoded form + and decrypt traffic (provided that it has the appropriate keying + material)." + </P> + + <P>ssldump is written by Eric Rescorla. + </P> +</DIV +> + +</BODY> +</HTML> diff --git a/epydoc.conf b/epydoc.conf new file mode 100644 index 0000000..2afa556 --- /dev/null +++ b/epydoc.conf @@ -0,0 +1,29 @@ +# Copyright 2009 Heikki Toivonen. All rights reserved. +# epydoc --no-private -v --config=epydoc.conf +[epydoc] + +name = M2Crypto +url = http://chandlerproject.org/Projects/MeTooCrypto + +output = html +target = doc/api +graph = all +sourcecode = no +private = no + +modules = ./M2Crypto + +# Private +exclude-introspect = M2Crypto.__m2crypto + +# Variable shadows module, which causes the module to double in the doc +# with second instance showing '. Exclude so we only get one (with '). +exclude = M2Crypto.PGP.PublicKey +exclude = M2Crypto.PGP.PublicKeyRing +exclude = M2Crypto.PGP.packet +exclude = M2Crypto.SSL.Cipher +exclude = M2Crypto.SSL.Connection +exclude = M2Crypto.SSL.Context +exclude = M2Crypto.SSL.SSLServer +exclude = M2Crypto.SSL.ssl_dispatcher +exclude = M2Crypto.SSL.timeout diff --git a/fedora_setup.sh b/fedora_setup.sh new file mode 100755 index 0000000..76d1f59 --- /dev/null +++ b/fedora_setup.sh @@ -0,0 +1,15 @@ +#!/bin/sh +# This script is meant to work around the differences on Fedora Core-based +# distributions (Redhat, CentOS, ...) compared to other common Linux +# distributions. +# +# Usage: ./fedora_setup.sh [setup.py options] +# + +arch=`uname -m` +for i in SWIG/_{ec,evp}.i; do + sed -i -e "s/opensslconf\./opensslconf-${arch}\./" "$i" +done + +SWIG_FEATURES=-cpperraswarn python setup.py $* + @@ -0,0 +1,37 @@ +#!/usr/bin/env python + +# Clean up M2Crypto source base. + +import glob, os, os.path, sys + +def zap(arg, dirname, names): + for f in glob.glob(dirname + arg): + try: + os.remove(f) + except: + pass + +if __name__ == "__main__": + start = sys.argv[1] + + os.path.walk(start, zap, "/*.pyc") + + if os.name == 'nt': + zap_m2 = ("__m2cryptoc.pyd","_m2crypto.py") + elif os.name == 'posix': + zap_m2 = ("__m2crypto.so","_m2crypto.py") + for x in zap_m2: + try: + os.remove("%s/M2Crypto/%s" % (start, x)) + except: + pass + + zap_swig = ("_m2crypto_wrap*", "_m2crypto.c", "_m2crypto.py", "vc60.pdb") + for x in zap_swig: + for z in glob.glob("%s/SWIG/%s" % (start, x)): + try: + os.remove(z) + except: + pass + + diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..861a9f5 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,5 @@ +[egg_info] +tag_build = +tag_date = 0 +tag_svn_revision = 0 + diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..e7c49eb --- /dev/null +++ b/setup.py @@ -0,0 +1,168 @@ +#!/usr/bin/env python + +""" +Distutils/setuptools installer for M2Crypto. + +Copyright (c) 1999-2004, Ng Pheng Siong. All rights reserved. + +Portions created by Open Source Applications Foundation (OSAF) are +Copyright (C) 2004-2007 OSAF. All Rights Reserved. + +Copyright 2008-2011 Heikki Toivonen. All rights reserved. +""" + +import os, sys +try: + from setuptools import setup + from setuptools.command import build_ext +except ImportError: + from distutils.core import setup + from distutils.command import build_ext + +from distutils.core import Extension + + +class _M2CryptoBuildExt(build_ext.build_ext): + '''Specialization of build_ext to enable swig_opts to inherit any + include_dirs settings made at the command line or in a setup.cfg file''' + user_options = build_ext.build_ext.user_options + \ + [('openssl=', 'o', 'Prefix for openssl installation location')] + + def initialize_options(self): + '''Overload to enable custom openssl settings to be picked up''' + + build_ext.build_ext.initialize_options(self) + + # openssl is the attribute corresponding to openssl directory prefix + # command line option + if os.name == 'nt': + self.libraries = ['ssleay32', 'libeay32'] + self.openssl = 'c:\\pkg' + else: + self.libraries = ['ssl', 'crypto'] + self.openssl = '/usr' + + + def finalize_options(self): + '''Overloaded build_ext implementation to append custom openssl + include file and library linking options''' + + build_ext.build_ext.finalize_options(self) + + opensslIncludeDir = os.path.join(self.openssl, 'include') + opensslLibraryDir = os.path.join(self.openssl, 'lib') + + self.swig_opts = ['-I%s' % i for i in self.include_dirs + \ + [opensslIncludeDir]] + self.swig_opts.append('-includeall') + #self.swig_opts.append('-D__i386__') # Uncomment for early OpenSSL 0.9.7 versions, or on Fedora Core if build fails + #self.swig_opts.append('-DOPENSSL_NO_EC') # Try uncommenting if you can't build with EC disabled + + self.include_dirs += [os.path.join(self.openssl, opensslIncludeDir), + os.path.join(os.getcwd(), 'SWIG')] + + if sys.platform == 'cygwin': + # Cygwin SHOULD work (there's code in distutils), but + # if one first starts a Windows command prompt, then bash, + # the distutils code does not seem to work. If you start + # Cygwin directly, then it would work even without this change. + # Someday distutils will be fixed and this won't be needed. + self.library_dirs += [os.path.join(self.openssl, 'bin')] + + self.library_dirs += [os.path.join(self.openssl, opensslLibraryDir)] + + +if sys.version_info < (2,4): + + # This copy of swig_sources is from Python 2.2. + + def swig_sources (self, sources): + + """Walk the list of source files in 'sources', looking for SWIG + interface (.i) files. Run SWIG on all that are found, and + return a modified 'sources' list with SWIG source files replaced + by the generated C (or C++) files. + """ + + new_sources = [] + swig_sources = [] + swig_targets = {} + + # XXX this drops generated C/C++ files into the source tree, which + # is fine for developers who want to distribute the generated + # source -- but there should be an option to put SWIG output in + # the temp dir. + + if self.swig_cpp: + target_ext = '.cpp' + else: + target_ext = '.c' + + for source in sources: + (base, ext) = os.path.splitext(source) + if ext == ".i": # SWIG interface file + new_sources.append(base + target_ext) + swig_sources.append(source) + swig_targets[source] = new_sources[-1] + else: + new_sources.append(source) + + if not swig_sources: + return new_sources + + swig = self.find_swig() + swig_cmd = [swig, "-python", "-ISWIG"] + if self.swig_cpp: + swig_cmd.append("-c++") + + swig_cmd += self.swig_opts + + for source in swig_sources: + target = swig_targets[source] + self.announce("swigging %s to %s" % (source, target)) + self.spawn(swig_cmd + ["-o", target, source]) + + return new_sources + + build_ext.build_ext.swig_sources = swig_sources + + +m2crypto = Extension(name = 'M2Crypto.__m2crypto', + sources = ['SWIG/_m2crypto.i'], + extra_compile_args = ['-DTHREADING'], + #extra_link_args = ['-Wl,-search_paths_first'], # Uncomment to build Universal Mac binaries + ) + +setup(name = 'M2Crypto', + version = '0.21.1', + description = 'M2Crypto: A Python crypto and SSL toolkit', + long_description = '''\ +M2Crypto is the most complete Python wrapper for OpenSSL featuring RSA, DSA, +DH, EC, HMACs, message digests, symmetric ciphers (including AES); SSL +functionality to implement clients and servers; HTTPS extensions to Python's +httplib, urllib, and xmlrpclib; unforgeable HMAC'ing AuthCookies for web +session management; FTP/TLS client and server; S/MIME; ZServerSSL: A HTTPS +server for Zope and ZSmime: An S/MIME messenger for Zope. M2Crypto can also be +used to provide SSL for Twisted.''', + license = 'BSD-style license', + platforms = ['any'], + author = 'Ng Pheng Siong', + author_email = 'ngps at sandbox rulemaker net', + maintainer = 'Heikki Toivonen', + maintainer_email = 'heikki@osafoundation.org', + url = 'http://chandlerproject.org/Projects/MeTooCrypto', + packages = ['M2Crypto', 'M2Crypto.SSL', 'M2Crypto.PGP'], + classifiers = [ + 'Development Status :: 5 - Production/Stable', + 'Intended Audience :: Developers', + 'Operating System :: OS Independent', + 'Programming Language :: C', + 'Programming Language :: Python', + 'Topic :: Security :: Cryptography', + 'Topic :: Software Development :: Libraries :: Python Modules', + ], + + ext_modules = [m2crypto], + test_suite='tests.alltests.suite', + cmdclass = {'build_ext': _M2CryptoBuildExt} + ) diff --git a/tests/README b/tests/README new file mode 100644 index 0000000..28eac67 --- /dev/null +++ b/tests/README @@ -0,0 +1,62 @@ +This directory contains unit tests for M2Crypto. + +To run all tests, make sure you have installed setuptools and then issue the +following command from the M2Crypto root directory: + +python setup.py test + +To run tests in a single file, for example test_ssl.py, do this: + +python setup.py test --test-suite=tests.test_ssl + + +Look also in the demo directory for other samples. + + +To create new test certificates: + +mkdir certs +cd certs + +Making the CA. You may want to use a locally edited openssl.cnf to +make sure that X509v3 Basic Constraints CA:TRUE gets set (by default +it may be false). By default duration may only be just one year; should +set this for at least 3 years. + +CA.sh -newca +cp demoCA/cacert.pem ../ca.pem + +Making the server certificate and private key. make sure commonName +field is localhost. + +CA.sh -newreq +CA.sh -signreq +cp newcert.pem ../server.pem +openssl rsa <newkey.pem >>../server.pem + +Making the x509 certificate and key. + +CA.sh -newreq +CA.sh -signreq +cp newcert.pem ../x509.pem +openssl rsa <newkey.pem >>../x509.pem +openssl x509 -in ../x509.pem -out ../x509.der -outform DER + +Making the signer certificate. Make sure the email address is +signer@example.com. + +CA.sh -newreq +CA.sh -signreq +cp newcert.pem ../signer.pem +openssl rsa <newkey.pem >../signer_key.pem + +Making the recipient certificate. Make sure the email address is +recipient@example.com. + +CA.sh -newreq +CA.sh -signreq +cp newcert.pem ../recipient.pem +openssl rsa <newkey.pem >../recipient_key.pem + + +Finally run the tests and edit for new values. diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/tests/__init__.py diff --git a/tests/alltests.py b/tests/alltests.py new file mode 100644 index 0000000..b35e58f --- /dev/null +++ b/tests/alltests.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python + +def suite(): + from M2Crypto import m2 + import os + import unittest + + def my_import(name): + # See http://docs.python.org/lib/built-in-funcs.html#l2h-6 + components = name.split('.') + try: + # python setup.py test + mod = __import__(name) + for comp in components[1:]: + mod = getattr(mod, comp) + except ImportError: + # python tests/alltests.py + mod = __import__(components[1]) + return mod + + modules_to_test = [ + 'tests.test_asn1', + 'tests.test_bio', + 'tests.test_bio_membuf', + 'tests.test_bio_file', + 'tests.test_bio_iobuf', + 'tests.test_bio_ssl', + 'tests.test_bn', + 'tests.test_authcookie', + 'tests.test_dh', + 'tests.test_dsa', + 'tests.test_engine', + 'tests.test_evp', + 'tests.test_obj', + 'tests.test_pgp', + 'tests.test_rand', + 'tests.test_rc4', + 'tests.test_rsa', + 'tests.test_smime', + 'tests.test_ssl_offline', + 'tests.test_threading', + 'tests.test_x509'] + if os.name == 'posix': + modules_to_test.append('tests.test_ssl') + elif os.name == 'nt': + modules_to_test.append('tests.test_ssl_win') + if m2.OPENSSL_VERSION_NUMBER >= 0x90800F and m2.OPENSSL_NO_EC == 0: + modules_to_test.append('tests.test_ecdh') + modules_to_test.append('tests.test_ecdsa') + modules_to_test.append('tests.test_ec_curves') + alltests = unittest.TestSuite() + for module in map(my_import, modules_to_test): + alltests.addTest(module.suite()) + return alltests + + +def dump_garbage(): + import gc + print '\nGarbage:' + gc.collect() + if len(gc.garbage): + + print '\nLeaked objects:' + for x in gc.garbage: + s = str(x) + if len(s) > 77: s = s[:73]+'...' + print type(x), '\n ', s + + print 'There were %d leaks.' % len(gc.garbage) + else: + print 'Python garbage collector did not detect any leaks.' + print 'However, it is still possible there are leaks in the C code.' + + +def runall(report_leaks=0): + report_leaks = report_leaks + + if report_leaks: + import gc + gc.enable() + gc.set_debug(gc.DEBUG_LEAK & ~gc.DEBUG_SAVEALL) + + import os, unittest + from M2Crypto import Rand + + try: + Rand.load_file('tests/randpool.dat', -1) + unittest.TextTestRunner(verbosity=2).run(suite()) + Rand.save_file('tests/randpool.dat') + finally: + if os.name == 'posix': + from test_ssl import zap_servers + zap_servers() + + if report_leaks: + dump_garbage() + + +if __name__ == '__main__': + runall(0) diff --git a/tests/ca.pem b/tests/ca.pem new file mode 100644 index 0000000..5450a68 --- /dev/null +++ b/tests/ca.pem @@ -0,0 +1,62 @@ +Certificate: + Data: + Version: 3 (0x2) + Serial Number: + d1:b6:bf:af:06:17:8c:bd + Signature Algorithm: sha1WithRSAEncryption + Issuer: C=US, ST=California, O=M2Crypto, CN=Heikki Toivonen + Validity + Not Before: Jul 28 04:30:50 2009 GMT + Not After : Jul 27 04:30:50 2012 GMT + Subject: C=US, ST=California, O=M2Crypto, CN=Heikki Toivonen + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + RSA Public Key: (1024 bit) + Modulus (1024 bit): + 00:c8:9b:59:18:c2:bf:21:68:dc:d4:62:30:1f:43: + 29:52:85:8d:36:fc:20:7f:11:1b:c6:f3:e6:c2:7a: + d0:17:0e:6e:78:43:21:e9:e2:df:9f:31:87:e8:7a: + 37:88:1f:a4:56:a1:e9:cb:13:7b:1b:c0:28:cf:5a: + db:a3:e7:50:6c:c6:55:76:e3:61:e8:73:4b:c2:8c: + ee:1c:29:c1:ee:2d:fd:e2:30:34:69:06:ea:d0:af: + bd:c5:db:86:70:92:26:0a:33:1b:70:a9:e7:6e:a4: + 2e:ee:4a:8a:f3:b2:6c:c9:97:28:39:28:28:3f:c5: + 90:4d:4e:83:0a:0e:cd:98:93 + Exponent: 65537 (0x10001) + X509v3 extensions: + X509v3 Subject Key Identifier: + AD:64:45:74:8F:83:C7:2C:D5:D7:A0:85:91:10:40:9A:9C:96:CF:EE + X509v3 Authority Key Identifier: + keyid:AD:64:45:74:8F:83:C7:2C:D5:D7:A0:85:91:10:40:9A:9C:96:CF:EE + DirName:/C=US/ST=California/O=M2Crypto/CN=Heikki Toivonen + serial:D1:B6:BF:AF:06:17:8C:BD + + X509v3 Basic Constraints: + CA:TRUE + Signature Algorithm: sha1WithRSAEncryption + c8:11:af:7d:6d:fb:1c:82:0d:c0:e7:41:f4:b2:a5:b0:69:6d: + 18:e3:04:aa:49:e6:4a:69:6d:c3:e3:8b:ab:d1:18:ac:72:ef: + 48:9e:49:c7:57:75:2d:00:1e:08:9f:c3:dc:ca:5f:91:38:0d: + ac:f8:1f:cc:fc:f7:c2:5b:ce:d7:0c:cf:b2:fe:c9:a9:ce:b8: + 07:45:17:1c:cf:b3:07:f9:1f:69:6a:94:03:be:62:62:9c:af: + a2:24:25:2d:1f:63:0a:91:6b:bb:e3:6c:ec:20:de:80:d3:04: + b4:5e:42:1f:27:bc:1f:79:98:18:ba:fb:8a:34:24:a9:40:1e: + b9:7b +-----BEGIN CERTIFICATE----- +MIICzjCCAjegAwIBAgIJANG2v68GF4y9MA0GCSqGSIb3DQEBBQUAME8xCzAJBgNV +BAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMREwDwYDVQQKEwhNMkNyeXB0bzEY +MBYGA1UEAxMPSGVpa2tpIFRvaXZvbmVuMB4XDTA5MDcyODA0MzA1MFoXDTEyMDcy +NzA0MzA1MFowTzELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExETAP +BgNVBAoTCE0yQ3J5cHRvMRgwFgYDVQQDEw9IZWlra2kgVG9pdm9uZW4wgZ8wDQYJ +KoZIhvcNAQEBBQADgY0AMIGJAoGBAMibWRjCvyFo3NRiMB9DKVKFjTb8IH8RG8bz +5sJ60BcObnhDIeni358xh+h6N4gfpFah6csTexvAKM9a26PnUGzGVXbjYehzS8KM +7hwpwe4t/eIwNGkG6tCvvcXbhnCSJgozG3Cp526kLu5KivOybMmXKDkoKD/FkE1O +gwoOzZiTAgMBAAGjgbEwga4wHQYDVR0OBBYEFK1kRXSPg8cs1deghZEQQJqcls/u +MH8GA1UdIwR4MHaAFK1kRXSPg8cs1deghZEQQJqcls/uoVOkUTBPMQswCQYDVQQG +EwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTERMA8GA1UEChMITTJDcnlwdG8xGDAW +BgNVBAMTD0hlaWtraSBUb2l2b25lboIJANG2v68GF4y9MAwGA1UdEwQFMAMBAf8w +DQYJKoZIhvcNAQEFBQADgYEAyBGvfW37HIINwOdB9LKlsGltGOMEqknmSmltw+OL +q9EYrHLvSJ5Jx1d1LQAeCJ/D3MpfkTgNrPgfzPz3wlvO1wzPsv7Jqc64B0UXHM+z +B/kfaWqUA75iYpyvoiQlLR9jCpFru+Ns7CDegNMEtF5CHye8H3mYGLr7ijQkqUAe +uXs= +-----END CERTIFICATE----- diff --git a/tests/der_encoded_seq.b64 b/tests/der_encoded_seq.b64 new file mode 100644 index 0000000..ece83d7 --- /dev/null +++ b/tests/der_encoded_seq.b64 @@ -0,0 +1,17 @@ +MIIDwTCCA70wggKloAMCAQICAg5QMA0GCSqGSIb3DQEBBQUAMGkxEzARBgoJkiaJk/IsZAEZFgNv +cmcxGDAWBgoJkiaJk/IsZAEZFghET0VHcmlkczEgMB4GA1UECxMXQ2VydGlmaWNhdGUgQXV0aG9y +aXRpZXMxFjAUBgNVBAMTDURPRUdyaWRzIENBIDEwHhcNMDUwMTEyMDA1MTAyWhcNMDYwMTEyMDA1 +MTAyWjBhMRMwEQYKCZImiZPyLGQBGRMDb3JnMRgwFgYKCZImiZPyLGQBGRMIZG9lZ3JpZHMxETAP +BgNVBAsTCFNlcnZpY2VzMR0wGwYDVQQDExRob3N0L2Jvc3Nob2cubGJsLmdvdjCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAJ+M2ICaiIkr1YcPzXlOSA/DTzduZPq+ALwbmU+nHatL8wXr +8SmwMq/aIG9egGnJzsTlofkSA5D7HAX3nLKkU0A+k0Eig2QzZ5iPjrtAr2m9/b/EsF2I58yOcEDT +EZuHQWCg19YE29n3UwqXB6pgpuMcFfFqLz6NydN9i0T7Md8OGwYpNzK6TsOCtyF+nin5mZleDyXF +BtkXeIDHmq8sYmsELv1fbAwYzpAZqvz3k0umXSMwnyq+z6WpawybNuLrhRzqMrMZYzq0Db/hS4xr +TeXOFuRQ/IcgWt0rLmOIRYKrVfLujFcAQnaPDW8llK2kgi6OrPlTo8e8JVEcz5d7bKUCAwEAAaN3 +MHUwEQYJYIZIAYb4QgEBBAQDAgbAMA4GA1UdDwEB/wQEAwIE8DAfBgNVHSMEGDAWgBTKGR0Sjm6k +OF1C1DEOCNvZjRcNXTAvBgNVHREEKDAmgg9ib3NzaG9nLmxibC5nb3aBE01LUm9kcmlndWV6QGxi +bC5nb3YwDQYJKoZIhvcNAQEFBQADggEBAKZkj+amwHCSdcMC05NBv1Vck46tGEsAEf9WiNvf3B1N +O+B0d+egcbXME+LgrxWvOIf4k2DKvORt/oz5BavEbmD1+/NUseBEA01bzjXXgmWwW48CK5cb6Qam +wq1uMa/asUgyLfpVkgA9ThwHIr2/g4HEIqUZLaqotLS5+EzZKhOuWh9OJh+AR6WCxOjTQw1xCJd0 +T/sSlHdtlH47xBkDL5DKe0pn9CgGY+ihehcRpzXMB8tKgxzsE8NuB49IUrVC+wIgzEXEeaSPFenL +XdWxAKaFWRXyPHrsrtsenxnRkM6y8qxWN6UV5UqpvdE/cy+5QORbzWtfVPYqtNVZoO+N3rc=
\ No newline at end of file diff --git a/tests/dhparams.pem b/tests/dhparams.pem new file mode 100644 index 0000000..edb320a --- /dev/null +++ b/tests/dhparams.pem @@ -0,0 +1,5 @@ +-----BEGIN DH PARAMETERS----- +MIGHAoGBAIgAcu3gpJeO8aS6N+sTMa655BMzBNlK66q62VH9RqmHwFJSjjCs3ZsF +aVsTkO3Mt0gULn1drXsK6Lc6pA8s0eQN8ggyEPVr6ND0jN2jr2qc1XlqD7jxzdxe +igB66pLvrWvAulrGxg4QMsQjqcpwZZ2ndRpYSmErIi4M1r19nTgjAgEC +-----END DH PARAMETERS----- diff --git a/tests/dsa.param.pem b/tests/dsa.param.pem new file mode 100644 index 0000000..6300c4f --- /dev/null +++ b/tests/dsa.param.pem @@ -0,0 +1,9 @@ +-----BEGIN DSA PARAMETERS----- +MIIBHgKBgQDqYu3smvs8KHBx4XX8otDHQUdtCyWKIRC52eQUSYeWejvdaqm11rIq +QMFBJDnvicM4avyOIU2d/ZOfLzN7aXFw3Ep67amsLpj82N+n4lASUJwKdOJyzyLG +9IS41RRek8B3lRAs/zqk1U6P5EaE/uIG4+avYmkSDpB4kmnRGhTYeQIVALKQpdTy +uat9aoq9mFm8dVA2VxAhAoGAausAkdN4Hj6S5nfPMjJTnwV4u7hVY6b6eAZTJmxs +ykr3XlKqM3PS0hKhjatp7f+mRNFxYvGrWPLAwnPOIp/iNn7lM7U41ceUj1O3KrD/ +Cg4LQ/bADvY48eytdjMLZDpL3Jwootfs4i8WIb0RnIxg8nClhkY2K2/+4voft+Xi +DlA= +-----END DSA PARAMETERS----- diff --git a/tests/dsa.priv.pem b/tests/dsa.priv.pem new file mode 100644 index 0000000..cd5578c --- /dev/null +++ b/tests/dsa.priv.pem @@ -0,0 +1,9 @@ +-----BEGIN PRIVATE KEY----- +MIIBSgIBADCCASsGByqGSM44BAEwggEeAoGBAOpi7eya+zwocHHhdfyi0MdBR20L +JYohELnZ5BRJh5Z6O91qqbXWsipAwUEkOe+Jwzhq/I4hTZ39k58vM3tpcXDcSnrt +qawumPzY36fiUBJQnAp04nLPIsb0hLjVFF6TwHeVECz/OqTVTo/kRoT+4gbj5q9i +aRIOkHiSadEaFNh5AhUAspCl1PK5q31qir2YWbx1UDZXECECgYBq6wCR03gePpLm +d88yMlOfBXi7uFVjpvp4BlMmbGzKSvdeUqozc9LSEqGNq2nt/6ZE0XFi8atY8sDC +c84in+I2fuUztTjVx5SPU7cqsP8KDgtD9sAO9jjx7K12MwtkOkvcnCii1+ziLxYh +vRGcjGDycKWGRjYrb/7i+h+35eIOUAQWAhQ/vK8oLKHdEu7W77fbZ3+jQvvt6Q== +-----END PRIVATE KEY----- diff --git a/tests/dsa.pub.pem b/tests/dsa.pub.pem new file mode 100644 index 0000000..34c8d45 --- /dev/null +++ b/tests/dsa.pub.pem @@ -0,0 +1,12 @@ +-----BEGIN PUBLIC KEY----- +MIIBtzCCASsGByqGSM44BAEwggEeAoGBAOpi7eya+zwocHHhdfyi0MdBR20LJYoh +ELnZ5BRJh5Z6O91qqbXWsipAwUEkOe+Jwzhq/I4hTZ39k58vM3tpcXDcSnrtqawu +mPzY36fiUBJQnAp04nLPIsb0hLjVFF6TwHeVECz/OqTVTo/kRoT+4gbj5q9iaRIO +kHiSadEaFNh5AhUAspCl1PK5q31qir2YWbx1UDZXECECgYBq6wCR03gePpLmd88y +MlOfBXi7uFVjpvp4BlMmbGzKSvdeUqozc9LSEqGNq2nt/6ZE0XFi8atY8sDCc84i +n+I2fuUztTjVx5SPU7cqsP8KDgtD9sAO9jjx7K12MwtkOkvcnCii1+ziLxYhvRGc +jGDycKWGRjYrb/7i+h+35eIOUAOBhQACgYEAmYO4Qss7pYq3vAEj5qj35A1gsOeC +rGxvu8w9Dj8jACuz0IHOTq+vFbnB6p30rdloMU7Ci4NHPWqOVmWCFcXxkFgJMJld +sBbkdbGnhPI80sWJGouUYofHFG4pK0QoMeDZKgg7OgwH8EnmQM+W9KDZYJRSuU4+ +6F6hQkmkwAKAXkU= +-----END PUBLIC KEY----- diff --git a/tests/ec.priv.pem b/tests/ec.priv.pem new file mode 100644 index 0000000..2147d67 --- /dev/null +++ b/tests/ec.priv.pem @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MG0CAQEEHXXhxMbflWHSfCjfxsqHTsIR+BVbREI6JFYGaUs0oAcGBSuBBAAaoUAD +PgAEAdJXSN/xnRiDqc4wSiYbWB7LGabs71Y9zzIE1ZbzAcvb7uxtoyUxrmRQC8xD +EO2qZX16mtpmgoNz3EeT +-----END EC PRIVATE KEY----- diff --git a/tests/ec.pub.pem b/tests/ec.pub.pem new file mode 100644 index 0000000..4a08b59 --- /dev/null +++ b/tests/ec.pub.pem @@ -0,0 +1,4 @@ +-----BEGIN PUBLIC KEY----- +MFIwEAYHKoZIzj0CAQYFK4EEABoDPgAEAdJXSN/xnRiDqc4wSiYbWB7LGabs71Y9 +zzIE1ZbzAcvb7uxtoyUxrmRQC8xDEO2qZX16mtpmgoNz3EeT +-----END PUBLIC KEY----- diff --git a/tests/fips.py b/tests/fips.py new file mode 100644 index 0000000..32eb75b --- /dev/null +++ b/tests/fips.py @@ -0,0 +1,8 @@ +try: + f = open('/proc/sys/crypto/fips_enabled') + try: + fips_mode = int(f.read()) + finally: + f.close() +except Exception, e: + fips_mode = 0 diff --git a/tests/long_serial_cert.pem b/tests/long_serial_cert.pem new file mode 100644 index 0000000..20ccac4 --- /dev/null +++ b/tests/long_serial_cert.pem @@ -0,0 +1,17 @@ +-----BEGIN CERTIFICATE----- +MIICsDCCAhmgAwIBAgIJAPR7mEmILgX6MA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV +BAYTAlVTMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX +aWRnaXRzIFB0eSBMdGQwHhcNMDkwNjE5MTkzNjIyWhcNMDkwNzE5MTkzNjIyWjBF +MQswCQYDVQQGEwJVUzETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50 +ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKB +gQDRb/jlXLidgXQGsOLoFbh4JAvC+BgufR7jn93KPybI0oo8VXFUqr2eFuLDcPiE +gpWIMrLwq9f0US/M/yXQdsH08L2xe+aaiNl+j+o4VsPhXfnvFvAtFRs+JqCR3VfI +vVePwov31+/28PmF1kOxr9SmSzvSPnN3SqSC0GDAmhWNYwIDAQABo4GnMIGkMB0G +A1UdDgQWBBR3SQBG5X/vH18obsb2aaBxhU/+HjB1BgNVHSMEbjBsgBR3SQBG5X/v +H18obsb2aaBxhU/+HqFJpEcwRTELMAkGA1UEBhMCVVMxEzARBgNVBAgTClNvbWUt +U3RhdGUxITAfBgNVBAoTGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZIIJAPR7mEmI +LgX6MAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADgYEAoyvUPgjkwPAgiqiU +4cD1tZdcxIzkk22U02cTjJtAPnIYwqwwDpFltApvRbp8MNiPqXbBt0tchj/Ovu4D +JiDTMaizVOZ+XmiC68uNTZ4nvEwHvVdKmudNVTZDdFr/a6BErAeTknlMCihN3v6M +POx8a1iz8Y/wJ8YA74vMPORKlKc= +-----END CERTIFICATE----- diff --git a/tests/pubring.pgp b/tests/pubring.pgp Binary files differnew file mode 100644 index 0000000..fbe4863 --- /dev/null +++ b/tests/pubring.pgp diff --git a/tests/recipient.pem b/tests/recipient.pem new file mode 100644 index 0000000..d766266 --- /dev/null +++ b/tests/recipient.pem @@ -0,0 +1,61 @@ +Certificate: + Data: + Version: 3 (0x2) + Serial Number: + d1:b6:bf:af:06:17:8c:c1 + Signature Algorithm: sha1WithRSAEncryption + Issuer: C=US, ST=California, O=M2Crypto, CN=Heikki Toivonen + Validity + Not Before: Jul 28 04:39:19 2009 GMT + Not After : Jul 26 04:39:19 2019 GMT + Subject: C=US, ST=California, O=M2Crypto, CN=Recipient/emailAddress=recipient@example.com + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + RSA Public Key: (1024 bit) + Modulus (1024 bit): + 00:c2:21:a3:4f:64:59:9c:21:39:21:d2:3c:e7:0a: + 60:72:c8:39:b3:c3:27:4a:6d:56:8f:a0:5d:1b:c6: + e4:3e:26:61:09:a9:ae:04:83:69:3f:9d:2b:12:7e: + d4:f7:8e:d0:6e:a9:8c:9b:d1:bf:17:0c:bd:d0:73: + 99:02:6e:7e:cb:7a:80:2d:cf:b1:29:c0:30:36:3f: + 68:12:3e:4e:bf:f9:8b:3d:1d:56:af:24:94:ae:d5: + 59:b4:00:50:0c:c0:2b:59:c3:99:b3:8a:19:f1:86: + 14:bd:ee:e9:c4:f1:d7:6a:0c:e9:67:8a:94:9a:2d: + 2d:60:25:22:c6:72:68:c2:0d + Exponent: 65537 (0x10001) + X509v3 extensions: + X509v3 Basic Constraints: + CA:FALSE + Netscape Comment: + OpenSSL Generated Certificate + X509v3 Subject Key Identifier: + 11:CB:60:AC:55:85:52:84:C5:C8:20:5A:50:13:D0:89:C7:7A:B7:81 + X509v3 Authority Key Identifier: + keyid:AD:64:45:74:8F:83:C7:2C:D5:D7:A0:85:91:10:40:9A:9C:96:CF:EE + + Signature Algorithm: sha1WithRSAEncryption + 87:56:17:6d:ba:3b:a6:c4:22:af:20:f1:a0:e5:9d:27:c4:50: + bd:79:eb:d2:84:e5:9a:00:5f:5d:5a:c3:34:58:77:f5:a9:00: + f9:76:e9:2d:89:b4:3f:9d:e3:cf:15:0c:64:1b:0a:03:db:e4: + 6f:2b:ff:1c:82:89:1a:0f:7e:83:58:0f:e6:da:af:26:97:49: + 4a:59:d7:61:3f:4b:ed:1d:5b:51:00:3b:83:96:c7:1e:3d:84: + f4:91:1f:70:69:12:b9:a7:2c:5b:1b:05:cd:74:90:2b:a0:ba: + e7:70:cd:6b:7d:ac:be:d7:92:50:e9:f5:c0:42:29:04:ef:8f: + a1:68 +-----BEGIN CERTIFICATE----- +MIICtzCCAiCgAwIBAgIJANG2v68GF4zBMA0GCSqGSIb3DQEBBQUAME8xCzAJBgNV +BAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMREwDwYDVQQKEwhNMkNyeXB0bzEY +MBYGA1UEAxMPSGVpa2tpIFRvaXZvbmVuMB4XDTA5MDcyODA0MzkxOVoXDTE5MDcy +NjA0MzkxOVowbzELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExETAP +BgNVBAoTCE0yQ3J5cHRvMRIwEAYDVQQDEwlSZWNpcGllbnQxJDAiBgkqhkiG9w0B +CQEWFXJlY2lwaWVudEBleGFtcGxlLmNvbTCBnzANBgkqhkiG9w0BAQEFAAOBjQAw +gYkCgYEAwiGjT2RZnCE5IdI85wpgcsg5s8MnSm1Wj6BdG8bkPiZhCamuBINpP50r +En7U947QbqmMm9G/Fwy90HOZAm5+y3qALc+xKcAwNj9oEj5Ov/mLPR1WrySUrtVZ +tABQDMArWcOZs4oZ8YYUve7pxPHXagzpZ4qUmi0tYCUixnJowg0CAwEAAaN7MHkw +CQYDVR0TBAIwADAsBglghkgBhvhCAQ0EHxYdT3BlblNTTCBHZW5lcmF0ZWQgQ2Vy +dGlmaWNhdGUwHQYDVR0OBBYEFBHLYKxVhVKExcggWlAT0InHereBMB8GA1UdIwQY +MBaAFK1kRXSPg8cs1deghZEQQJqcls/uMA0GCSqGSIb3DQEBBQUAA4GBAIdWF226 +O6bEIq8g8aDlnSfEUL1569KE5ZoAX11awzRYd/WpAPl26S2JtD+d488VDGQbCgPb +5G8r/xyCiRoPfoNYD+baryaXSUpZ12E/S+0dW1EAO4OWxx49hPSRH3BpErmnLFsb +Bc10kCuguudwzWt9rL7XklDp9cBCKQTvj6Fo +-----END CERTIFICATE----- diff --git a/tests/recipient_key.pem b/tests/recipient_key.pem new file mode 100644 index 0000000..5bf0f70 --- /dev/null +++ b/tests/recipient_key.pem @@ -0,0 +1,15 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICXAIBAAKBgQDCIaNPZFmcITkh0jznCmByyDmzwydKbVaPoF0bxuQ+JmEJqa4E +g2k/nSsSftT3jtBuqYyb0b8XDL3Qc5kCbn7LeoAtz7EpwDA2P2gSPk6/+Ys9HVav +JJSu1Vm0AFAMwCtZw5mzihnxhhS97unE8ddqDOlnipSaLS1gJSLGcmjCDQIDAQAB +AoGAZlrJ+kAUpyc1Mkng5ogoFhzPn6ITg0Bm1U9eCBkzmjkuDKQ0JhkLUwkQ/q10 +qBnad55ZjoZmVEbZhaCNWiTcIIy0nKAMWNKRcg3vTgrnbmbjco1HECDStfJKogZl +7egoIImHnU1f/IeKQDUYUfs/INonmnnZ1d2jrU7QsdTz84ECQQDzhT0UwP8S1oma +0IBgeUOt5ptZs7nFdZnbIKCd+ADra6NiQznokCHe5K0WZHqPKvN9asKx1u0h+97H +Wmk6Fw7RAkEAzBR1+mTRSrlJT8/NTCsIDPtCK/+OhmGbNy1pfsOWq1lN58Za5HV7 +fmtaH2No+MP+DlfNigsg557GzAYl2ZumfQJAHQj33W+dehuGUKUniVksDqH+R9W8 +AqUg8RWU0QDu6yLsWhz13JrCzxao5JCaZFOUsJF4IUglAfZL+6z1+u0g4QJAH5aL +LFaujoJfdpsTi9adSGUbuPO1e9dfzwqYaaaci6knBdkN+I62rrqvGGyqstajXFT6 +24MddLx+yNWqxiPxgQJBAKF8YiR4eLqLSnq4ftqCqVCC1XbA2H9b7G5RBWi00WFq +3Nx+B/wjLzbqsMamTCIDUCEW+MzFx6otCxduDZRMKH8= +-----END RSA PRIVATE KEY----- diff --git a/tests/rsa.priv.pem b/tests/rsa.priv.pem new file mode 100644 index 0000000..0b8b163 --- /dev/null +++ b/tests/rsa.priv.pem @@ -0,0 +1,16 @@ +-----BEGIN PRIVATE KEY----- +MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAM1lIRXaaLVgzlvW +F2S6OMFJsfG+coZLx9qzmNb2gK6qjyGa71HeaLvFmQFv60dPjpuaGPs2uhL88hcN +JAChGiD8LxNpVW0EEw+RRH6/CBlDGuKjkSaPz8zzpEhSZq/yGb0F4zaau1HINnwo +rYPyRXWyRUzfpEB/7mx8/FUD24knAgMBAAECgYAaInsSP8dBBP9c+iHh5DwihBEL +VJNX+T6F2oJhH96B2xv5R7CZ9zXWZq8wWqBSY5IexH3XQUBt+BeJzVc+aUFcpKLM +D1O3OZ8NwC9HGIY0sLeX+uawYdFAPJfF8BZ8x3LMxWA8jdJM+4/P3C3jh2EvyzLT +HQ1rXBPrLkH45xJQSQJBAPPfSiObRvbeJvkgE0z5SqdbQjTGxeAX5qt1e2LtTwdU +gAxpYnYPz9CFCcIpa0j9UejlGninXasQvhpdwytBLk0CQQDXm/2kKh9BuWzemIu/ +QcLSgc7VrGgZnGQ27fp3bXDSOV3Bev9VywLQ5VDBJcRSkMTC0V/+iHZbMl9EpwHN +8ZdDAkBJHtAZ8PrMFjvVQnrG/5AUsdYeAONfl4sAKc9/D+w8JGfoUMjG4WLMALe2 +UbjrP5kJnXfcaUI6gmCdgzN7iqWZAkAvJbpKOrfJDH4lEuCEOyIaHC6ZhPDioNM9 +O77ofLMOFWNOGtJY9WKxQWPuSI7sqyGLpHNEWpzfBl3UylxXp3u3AkEAzCzGUMfM +0qw/TFlVLzCHhrkP13SrJohdD693w9nuhYM2u27R32qJlF1OvN9NxEV7ZoOSGJxi +CGTjWcXUGgYQgQ== +-----END PRIVATE KEY----- diff --git a/tests/rsa.priv2.pem b/tests/rsa.priv2.pem new file mode 100644 index 0000000..013ab5e --- /dev/null +++ b/tests/rsa.priv2.pem @@ -0,0 +1,17 @@ +-----BEGIN ENCRYPTED PRIVATE KEY----- +MIICxjBABgkqhkiG9w0BBQ0wMzAbBgkqhkiG9w0BBQwwDgQIq+j6kBSOkTkCAggA +MBQGCCqGSIb3DQMHBAin1qbPaI3dAQSCAoAwb9BTWY6+o9GAZk9ZUJHAHL0Yb7C/ +Hkm8Kh+YBqIEHbTzzSzIO3pFFnLrLLSVbWuYX3bBJRDSUfmV9JaZu0YYJ/TzBtb5 +epgD+sZ83E11NM0L3rJTI9GOUm8b9U15N94X+gnQj0JSK8Ex0dJpJ3rwHPd1zAOe +0SjXViOCCuHeu4Mnz3P9B42FR5C/53GLkqtSZCsznSBsbPGZ/mb6eEGjgYtxFm15 +17Px7ezDjjr5knBozYua3OehCfI6lN1W+yyTvHGF4lpWkm7Pj24uHHh6yagFQuvB +RgE8eFLLPLBBa3kHWTn6hAPL4pfPIaPiDtX69IshSv2LVcbUPp6pTkji7mo3EFpN +Jigd3msMCf6w5Wh4I2k8Hb6eSkfsModIru05xq0fuTYi1nTh2l/M3FEGeOuBmpbD +AYzpT6J1+373rshkdqmv1C/REsnnrACGwbM7JN6K3sKnJZesI3iiHY5tnumypyv3 +f7wMaRcIq0QOi/WUIKzU0B4f9WxgjDuFwWyYlEBl2IYZ8wxD0P2s968puc7RRwrc +11Tn0a993122gBAHaa24iAW2ig2hGktLtxY1EvY6Sfd/migu2iVA6bwdVz68kKBj +tYfJQEoMGJhR+NqSDYvgJYgoNljOIf6Wq++L9/zqgtYkiL7xRLqSvths2NWaxGmc +RvjWFeq2sTiVXFn36jzO9YfJ4BFqgt5UoBRSw8jYQwm+W5TUhgWGQxQTTrCUs/36 +5oQXOwpRol+ivO/VtMdDShg6sKHEjQ/FhHqNpPccVLg/g81HbJyfmEmeqYu6rtOd +xBe9lVFW+86wObsYl1WCHYUQuBUlPv+uEDLqC92/6zLdCtDYYRYvdLF8 +-----END ENCRYPTED PRIVATE KEY----- diff --git a/tests/rsa.pub.pem b/tests/rsa.pub.pem new file mode 100644 index 0000000..e4f13e0 --- /dev/null +++ b/tests/rsa.pub.pem @@ -0,0 +1,6 @@ +-----BEGIN PUBLIC KEY----- +MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDNZSEV2mi1YM5b1hdkujjBSbHx +vnKGS8fas5jW9oCuqo8hmu9R3mi7xZkBb+tHT46bmhj7NroS/PIXDSQAoRog/C8T +aVVtBBMPkUR+vwgZQxrio5Emj8/M86RIUmav8hm9BeM2mrtRyDZ8KK2D8kV1skVM +36RAf+5sfPxVA9uJJwIDAQAB +-----END PUBLIC KEY----- diff --git a/tests/server.pem b/tests/server.pem new file mode 100644 index 0000000..825abc6 --- /dev/null +++ b/tests/server.pem @@ -0,0 +1,75 @@ +Certificate: + Data: + Version: 3 (0x2) + Serial Number: + d1:b6:bf:af:06:17:8c:be + Signature Algorithm: sha1WithRSAEncryption + Issuer: C=US, ST=California, O=M2Crypto, CN=Heikki Toivonen + Validity + Not Before: Jul 28 04:31:41 2009 GMT + Not After : Jul 26 04:31:41 2019 GMT + Subject: C=US, ST=California, O=M2Crypto, CN=localhost + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + RSA Public Key: (1024 bit) + Modulus (1024 bit): + 00:d4:99:6f:33:3f:e6:ac:0a:34:d8:0e:45:97:f3: + 2b:6a:50:2a:84:30:0a:52:9c:15:30:9f:05:29:3a: + 21:f4:c1:c3:01:9e:2f:55:56:4e:35:ac:f1:16:1e: + 26:8d:b5:26:b7:99:78:92:ea:1c:74:46:ab:41:12: + ef:cc:53:62:cc:59:5c:9e:c4:86:df:d9:25:35:55: + 05:4b:16:ff:d9:90:e3:f4:51:b4:b4:fa:c5:98:4b: + 60:f0:60:7f:14:4e:1e:dd:61:9b:22:a2:9c:21:17: + 43:a3:cb:07:80:f5:75:59:9c:55:1c:fe:e0:66:d4: + 70:77:5e:13:06:0c:05:c7:1f + Exponent: 65537 (0x10001) + X509v3 extensions: + X509v3 Basic Constraints: + CA:FALSE + Netscape Comment: + OpenSSL Generated Certificate + X509v3 Subject Key Identifier: + 04:05:3D:6A:A7:E8:D7:52:BD:2F:C4:52:30:7C:2C:BD:D3:81:46:C6 + X509v3 Authority Key Identifier: + keyid:AD:64:45:74:8F:83:C7:2C:D5:D7:A0:85:91:10:40:9A:9C:96:CF:EE + + Signature Algorithm: sha1WithRSAEncryption + ac:2b:ad:86:36:96:5c:fb:34:2c:02:ca:d9:5f:a7:8e:b6:58: + 24:1d:27:b6:8e:81:aa:69:0e:60:26:64:2e:72:a1:ff:d8:ba: + bb:7e:5d:46:c7:07:2d:a8:c8:4c:df:1e:ba:c8:bc:21:5b:f2: + b3:01:4c:d6:3b:10:fd:49:70:e6:83:01:f3:24:e2:a9:97:d7: + c3:9c:5b:2d:d7:64:2b:e5:e2:0e:3e:d9:8c:e6:93:86:39:32: + 50:43:5f:36:4a:3b:b0:05:e7:65:a3:b3:ef:50:56:7f:7e:dc: + f0:65:83:ac:42:7e:97:a0:c0:7e:63:c6:c8:c6:35:d3:60:d1: + 4f:51 +-----BEGIN CERTIFICATE----- +MIICkTCCAfqgAwIBAgIJANG2v68GF4y+MA0GCSqGSIb3DQEBBQUAME8xCzAJBgNV +BAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMREwDwYDVQQKEwhNMkNyeXB0bzEY +MBYGA1UEAxMPSGVpa2tpIFRvaXZvbmVuMB4XDTA5MDcyODA0MzE0MVoXDTE5MDcy +NjA0MzE0MVowSTELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExETAP +BgNVBAoTCE0yQ3J5cHRvMRIwEAYDVQQDEwlsb2NhbGhvc3QwgZ8wDQYJKoZIhvcN +AQEBBQADgY0AMIGJAoGBANSZbzM/5qwKNNgORZfzK2pQKoQwClKcFTCfBSk6IfTB +wwGeL1VWTjWs8RYeJo21JreZeJLqHHRGq0ES78xTYsxZXJ7Eht/ZJTVVBUsW/9mQ +4/RRtLT6xZhLYPBgfxROHt1hmyKinCEXQ6PLB4D1dVmcVRz+4GbUcHdeEwYMBccf +AgMBAAGjezB5MAkGA1UdEwQCMAAwLAYJYIZIAYb4QgENBB8WHU9wZW5TU0wgR2Vu +ZXJhdGVkIENlcnRpZmljYXRlMB0GA1UdDgQWBBQEBT1qp+jXUr0vxFIwfCy904FG +xjAfBgNVHSMEGDAWgBStZEV0j4PHLNXXoIWREECanJbP7jANBgkqhkiG9w0BAQUF +AAOBgQCsK62GNpZc+zQsAsrZX6eOtlgkHSe2joGqaQ5gJmQucqH/2Lq7fl1Gxwct +qMhM3x66yLwhW/KzAUzWOxD9SXDmgwHzJOKpl9fDnFst12Qr5eIOPtmM5pOGOTJQ +Q182SjuwBedlo7PvUFZ/ftzwZYOsQn6XoMB+Y8bIxjXTYNFPUQ== +-----END CERTIFICATE----- +-----BEGIN RSA PRIVATE KEY----- +MIICXgIBAAKBgQDUmW8zP+asCjTYDkWX8ytqUCqEMApSnBUwnwUpOiH0wcMBni9V +Vk41rPEWHiaNtSa3mXiS6hx0RqtBEu/MU2LMWVyexIbf2SU1VQVLFv/ZkOP0UbS0 ++sWYS2DwYH8UTh7dYZsiopwhF0OjyweA9XVZnFUc/uBm1HB3XhMGDAXHHwIDAQAB +AoGBALBHrSm8kYMTT2/anZ/5tIUJhcdnohePbg6LvJbLqf4tb4l25V6IGn9tL9Yc +F/GmRD02VwDSd9d+BWAG2Kj+d0rfdCLfKY9O8PVVm0DF6grLZ7ugItYqUHRDYOdV +MOVOQrx+mCIzHtoEtQ6HLqmqt2rIX731L1TA7OLNm3XHyISJAkEA/mgNNNg0e23G +64z83yxxwPEnBrnKd1+xjH9QJ0Z9SJJuF4sNXRIFA4YUNvv2MNe3gMS4Hg9w78HL +PwcEzLnO9QJBANXuWAZGV58CdkM2w7H9+ukxMbQeLSnmgjpdddo31qqbfgFAYZMK +LppRqyosj+a2qQ6vua0ndstTImSi7KPmCUMCQQDbwr5Fu836ISYIK830aswIw0fX +A37mB3+zwfZXNwjaO8NmCvQMRZiXJqcnqBdOsckOLuBs9yGzuk/7rfBzeL5RAkA2 +uBcly7o/vsZ3HLvjfB5ApUecVZehvwcSXLN3VI8A5nLNaSVMEe+nozoPuIQ6NAB7 +9DCe/JgjG6mRaibzKTS3AkEAjTl5MTKkYR78+2u3NRU/ypa1iKCicSvI/Ryw7p/z +Q8XmVA0CmNRvltf9gA1gJ04ZijBPtl+s09uppaCw9L3vuA== +-----END RSA PRIVATE KEY----- diff --git a/tests/signer.pem b/tests/signer.pem new file mode 100644 index 0000000..816f5fa --- /dev/null +++ b/tests/signer.pem @@ -0,0 +1,61 @@ +Certificate: + Data: + Version: 3 (0x2) + Serial Number: + d1:b6:bf:af:06:17:8c:c0 + Signature Algorithm: sha1WithRSAEncryption + Issuer: C=US, ST=California, O=M2Crypto, CN=Heikki Toivonen + Validity + Not Before: Jul 28 04:37:25 2009 GMT + Not After : Jul 26 04:37:25 2019 GMT + Subject: C=US, ST=California, O=M2Crypto, CN=Signer/emailAddress=signer@example.com + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + RSA Public Key: (1024 bit) + Modulus (1024 bit): + 00:c3:9c:76:f3:21:aa:10:19:9f:77:e3:82:1d:9d: + c3:4a:da:bc:c3:83:71:d1:89:78:8b:82:a4:b9:c5: + 70:bb:e3:00:bf:49:b8:99:96:67:0b:bf:fe:72:cb: + d9:b6:63:85:f4:fb:86:55:32:22:1e:6e:ce:fd:88: + 5c:75:9d:77:3c:92:17:c5:b2:70:04:59:02:33:ef: + be:33:26:f1:e4:72:41:45:72:f1:bf:c4:21:b1:fe: + de:92:b9:f3:25:3e:1a:15:4b:26:47:29:cc:38:7f: + 58:3b:ae:b7:c5:69:e7:48:81:b6:55:61:45:c3:3f: + b6:9d:06:e5:17:41:f6:f2:e9 + Exponent: 65537 (0x10001) + X509v3 extensions: + X509v3 Basic Constraints: + CA:FALSE + Netscape Comment: + OpenSSL Generated Certificate + X509v3 Subject Key Identifier: + 22:CA:29:B7:D7:39:B4:BF:35:F9:36:5E:EE:2B:E4:17:4E:F9:6E:EE + X509v3 Authority Key Identifier: + keyid:AD:64:45:74:8F:83:C7:2C:D5:D7:A0:85:91:10:40:9A:9C:96:CF:EE + + Signature Algorithm: sha1WithRSAEncryption + 5f:a0:da:6b:37:b4:bb:25:34:a7:ed:f3:f7:2e:f2:85:aa:91: + 01:8f:c3:80:e5:44:87:df:9e:64:5e:5f:3e:5c:7f:c1:07:12: + 2a:46:cc:bb:9f:a4:a5:c8:3f:84:9a:a4:9e:d5:26:33:af:b4: + 5f:eb:8e:7d:81:65:f6:44:18:78:89:17:74:fb:07:dc:04:65: + fa:15:0c:b2:f3:e7:e7:af:1f:d9:02:c4:c4:44:b7:95:91:47: + fe:c0:2a:e1:7a:ae:dd:5f:f8:a9:fa:bb:dd:89:2d:0b:05:b6: + ce:ba:12:37:7f:97:4c:48:a9:fb:d4:b7:a5:d1:61:f6:85:ea: + 30:8c +-----BEGIN CERTIFICATE----- +MIICsTCCAhqgAwIBAgIJANG2v68GF4zAMA0GCSqGSIb3DQEBBQUAME8xCzAJBgNV +BAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMREwDwYDVQQKEwhNMkNyeXB0bzEY +MBYGA1UEAxMPSGVpa2tpIFRvaXZvbmVuMB4XDTA5MDcyODA0MzcyNVoXDTE5MDcy +NjA0MzcyNVowaTELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExETAP +BgNVBAoTCE0yQ3J5cHRvMQ8wDQYDVQQDEwZTaWduZXIxITAfBgkqhkiG9w0BCQEW +EnNpZ25lckBleGFtcGxlLmNvbTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA +w5x28yGqEBmfd+OCHZ3DStq8w4Nx0Yl4i4KkucVwu+MAv0m4mZZnC7/+csvZtmOF +9PuGVTIiHm7O/YhcdZ13PJIXxbJwBFkCM+++Mybx5HJBRXLxv8Qhsf7ekrnzJT4a +FUsmRynMOH9YO663xWnnSIG2VWFFwz+2nQblF0H28ukCAwEAAaN7MHkwCQYDVR0T +BAIwADAsBglghkgBhvhCAQ0EHxYdT3BlblNTTCBHZW5lcmF0ZWQgQ2VydGlmaWNh +dGUwHQYDVR0OBBYEFCLKKbfXObS/Nfk2Xu4r5BdO+W7uMB8GA1UdIwQYMBaAFK1k +RXSPg8cs1deghZEQQJqcls/uMA0GCSqGSIb3DQEBBQUAA4GBAF+g2ms3tLslNKft +8/cu8oWqkQGPw4DlRIffnmReXz5cf8EHEipGzLufpKXIP4SapJ7VJjOvtF/rjn2B +ZfZEGHiJF3T7B9wEZfoVDLLz5+evH9kCxMREt5WRR/7AKuF6rt1f+Kn6u92JLQsF +ts66Ejd/l0xIqfvUt6XRYfaF6jCM +-----END CERTIFICATE----- diff --git a/tests/signer_key.pem b/tests/signer_key.pem new file mode 100644 index 0000000..39abda1 --- /dev/null +++ b/tests/signer_key.pem @@ -0,0 +1,15 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICXAIBAAKBgQDDnHbzIaoQGZ9344IdncNK2rzDg3HRiXiLgqS5xXC74wC/SbiZ +lmcLv/5yy9m2Y4X0+4ZVMiIebs79iFx1nXc8khfFsnAEWQIz774zJvHkckFFcvG/ +xCGx/t6SufMlPhoVSyZHKcw4f1g7rrfFaedIgbZVYUXDP7adBuUXQfby6QIDAQAB +AoGAZL24JQ85XoFTt5Lb+BS/91Uf0jFn9Nov0um9nE8q+Bi40ctN3wuulkaS7Nw/ +i8dFvh2r2USwfavjvn7z3z7xoMG8V2c1ZFJCI2CKjocuWVkGwNnIsbO7/BOG03nu +vir/i7TXN0YbN8zMhfuFC9APmR8bdmMa2KgHXzQcLuAmI4ECQQDhDIkC97l6rMKG +QWbYrbc7GoMZNwCsPb/fasUknGmtPmq+s818i335u1yyhAk5pwKV7HF+WyZ76S2A +P1bZf9+FAkEA3oN98qoklVmWSK0qV+CKHjZHSqtt32q2eu6+eAO5fVZOWHwXhS/B +MkTtfKJbIDTLyUnwhKyht/hXOniVqHE5FQJAf99VgoArvc6oAQzsWTXrpQOddhhQ +o426lkHenrzZNvz+PjmACsJf5CRXuX9Ylo+U4ockvb0hEssddX+H47HK2QJBAIYr +aV1SJH79pvWpnLeiSAYRmok2tyiZMvELVkQNkuI1kUYfhRslAWxrTXvyddoEm8CC +2glWAqlokEhMf4kyxEUCQCIQbV+XFoEqkECchik34PPmcPi2ends32dv/sW+AKjQ +pxKpWbxVB4sEOPZzpmujP0LLxvCY4HOUJDlhENGQ8MM= +-----END RSA PRIVATE KEY----- diff --git a/tests/test_asn1.py b/tests/test_asn1.py new file mode 100644 index 0000000..30ef759 --- /dev/null +++ b/tests/test_asn1.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python + +"""Unit tests for M2Crypto.ASN1. + +Copyright (c) 2005 Open Source Applications Foundation. All rights reserved.""" + +import unittest, time, datetime +from M2Crypto import ASN1, m2 + +class ASN1TestCase(unittest.TestCase): + + def test_Integer(self): + pass # XXX Dunno how to test + + def test_BitSTring(self): + pass # XXX Dunno how to test + + def test_String(self): + asn1ptr = m2.asn1_string_new() + text = 'hello there' + # In RFC2253 format: + # #040B68656C6C6F207468657265 + # h e l l o t h e r e + m2.asn1_string_set(asn1ptr, text) + a = ASN1.ASN1_String(asn1ptr, 1) + assert a.as_text() == 'hello there', a.as_text() + assert a.as_text(flags=m2.ASN1_STRFLGS_RFC2253) == '#040B68656C6C6F207468657265', a.as_text(flags=m2.ASN1_STRFLGS_RFC2253) + self.assertEqual(a.as_text(), str(a)) + + def test_Object(self): + pass # XXX Dunno how to test + + def test_UTCTIME(self): + asn1 = ASN1.ASN1_UTCTIME() + assert str(asn1) == 'Bad time value' + + format = '%b %d %H:%M:%S %Y GMT' + utcformat = '%y%m%d%H%M%SZ' + + s = '990807053011Z' + asn1.set_string(s) + #assert str(asn1) == 'Aug 7 05:30:11 1999 GMT' + t1 = time.strptime(str(asn1), format) + t2 = time.strptime(s, utcformat) + self.assertEqual(t1, t2) + + asn1.set_time(500) + #assert str(asn1) == 'Jan 1 00:08:20 1970 GMT' + t1 = time.strftime(format, time.strptime(str(asn1), format)) + t2 = time.strftime(format, time.gmtime(500)) + self.assertEqual(t1, t2) + + t = long(time.time()) + time.timezone + asn1.set_time(t) + t1 = time.strftime(format, time.strptime(str(asn1), format)) + t2 = time.strftime(format, time.gmtime(t)) + self.assertEqual(t1, t2) + + def test_UTCTIME_datetime(self): + asn1 = ASN1.ASN1_UTCTIME() + # Test get_datetime and set_datetime + t = time.time() + dt = datetime.datetime.fromtimestamp(int(t)) + udt = dt.replace(tzinfo=ASN1.LocalTimezone()).astimezone(ASN1.UTC) + asn1.set_time(int(t)) + t1 = str(asn1) + asn1.set_datetime(dt) + t2 = str(asn1) + self.assertEqual(t1, t2) + self.assertEqual(str(udt), str(asn1.get_datetime())) + + dt = dt.replace(tzinfo=ASN1.LocalTimezone()) + asn1.set_datetime(dt) + t2 = str(asn1) + self.assertEqual(t1, t2) + self.assertEqual(str(udt), str(asn1.get_datetime())) + + dt = dt.astimezone(ASN1.UTC) + asn1.set_datetime(dt) + t2 = str(asn1) + self.assertEqual(t1, t2) + self.assertEqual(str(udt), str(asn1.get_datetime())) + + +def suite(): + return unittest.makeSuite(ASN1TestCase) + + +if __name__ == '__main__': + unittest.TextTestRunner().run(suite()) diff --git a/tests/test_authcookie.py b/tests/test_authcookie.py new file mode 100644 index 0000000..9ce0eb8 --- /dev/null +++ b/tests/test_authcookie.py @@ -0,0 +1,147 @@ +#!/usr/bin/env python + +"""Unit tests for M2Crypto.AuthCookie. + +Copyright (c) 1999-2002 Ng Pheng Siong. All rights reserved.""" + +import Cookie, binascii, time, unittest, sys +from M2Crypto.AuthCookie import AuthCookie, AuthCookieJar, mix, unmix, unmix3 +from M2Crypto import Rand, EVP + +class AuthCookieTestCase(unittest.TestCase): + + _format = 'Set-Cookie: _M2AUTH_="exp=%s&data=%s&digest=%s"' + if sys.version_info < (2,5): + _format += ';' + _token = '_M2AUTH_' + + def setUp(self): + self.data = 'cogitoergosum' + self.exp = time.time() + 3600 + self.jar = AuthCookieJar() + + def tearDown(self): + pass + + def test_mix_unmix(self): + dough = mix(self.exp, self.data) + exp, data = unmix(dough) + self.failUnlessEqual(data, self.data) + self.failUnlessEqual(exp, self.exp) + + def test_make_cookie(self): + c = self.jar.makeCookie(self.exp, self.data) + self.failUnless(isinstance(c, AuthCookie)) + self.failUnlessEqual(c.expiry(), self.exp) + self.failUnlessEqual(c.data(), self.data) + # Peek inside the cookie jar... + key = self.jar._key + mac = binascii.b2a_base64(EVP.hmac(key, mix(self.exp, self.data), 'sha1'))[:-1] + self.failUnlessEqual(c.mac(), mac) + # Ok, stop peeking now. + cookie_str = self._format % (repr(self.exp), self.data, mac) + self.failUnlessEqual(c.output(), cookie_str) + + def test_expired(self): + t = self.exp - 7200 + c = self.jar.makeCookie(t, self.data) + self.failUnless(c.isExpired()) + + def test_not_expired(self): + c = self.jar.makeCookie(self.exp, self.data) + self.failIf(c.isExpired()) + + def test_is_valid(self): + c = self.jar.makeCookie(self.exp, self.data) + self.failUnless(self.jar.isGoodCookie(c)) + + def test_is_invalid_expired(self): + t = self.exp - 7200 + c = self.jar.makeCookie(t, self.data) + self.failIf(self.jar.isGoodCookie(c)) + + def test_is_invalid_changed_exp(self): + c = self.jar.makeCookie(self.exp, self.data) + c._expiry = 'this is bad' + self.failIf(self.jar.isGoodCookie(c)) + + def test_is_invalid_changed_data(self): + c = self.jar.makeCookie(self.exp, self.data) + c._data = 'this is bad' + self.failIf(self.jar.isGoodCookie(c)) + + def test_is_invalid_changed_mac(self): + c = self.jar.makeCookie(self.exp, self.data) + c._mac = 'this is bad' + self.failIf(self.jar.isGoodCookie(c)) + + def test_mix_unmix3(self): + c = self.jar.makeCookie(self.exp, self.data) + s = Cookie.SmartCookie() + s.load(c.output()) + exp, data, digest = unmix3(s[self._token].value) + self.failUnlessEqual(data, self.data) + self.failUnlessEqual(float(exp), self.exp) + key = self.jar._key # Peeking... + mac = binascii.b2a_base64(EVP.hmac(key, mix(self.exp, self.data), 'sha1'))[:-1] + self.failUnlessEqual(digest, mac) + + def test_cookie_str(self): + c = self.jar.makeCookie(self.exp, self.data) + self.failUnless(self.jar.isGoodCookieString(c.output())) + + def test_cookie_str2(self): + c = self.jar.makeCookie(self.exp, self.data) + s = Cookie.SmartCookie() + s.load(c.output()) + self.failUnless(self.jar.isGoodCookieString(s.output())) + + def test_cookie_str_expired(self): + t = self.exp - 7200 + c = self.jar.makeCookie(t, self.data) + s = Cookie.SmartCookie() + s.load(c.output()) + self.failIf(self.jar.isGoodCookieString(s.output())) + + def test_cookie_str_arbitrary_change(self): + c = self.jar.makeCookie(self.exp, self.data) + cout = c.output() + str = cout[:32] + 'this is bad' + cout[32:] + s = Cookie.SmartCookie() + s.load(str) + self.failIf(self.jar.isGoodCookieString(s.output())) + + def test_cookie_str_changed_exp(self): + c = self.jar.makeCookie(self.exp, self.data) + cout = c.output() + str = cout[:26] + '2' + cout[27:] + s = Cookie.SmartCookie() + s.load(str) + self.failIf(self.jar.isGoodCookieString(s.output())) + + def test_cookie_str_changed_data(self): + c = self.jar.makeCookie(self.exp, self.data) + cout = c.output() + str = cout[:36] + 'X' + cout[37:] + s = Cookie.SmartCookie() + s.load(str) + self.failIf(self.jar.isGoodCookieString(s.output())) + + def test_cookie_str_changed_mac(self): + c = self.jar.makeCookie(self.exp, self.data) + cout = c.output() + str = cout[:76] + 'X' + cout[77:] + s = Cookie.SmartCookie() + s.load(str) + self.failIf(self.jar.isGoodCookieString(s.output())) + + +def suite(): + return unittest.makeSuite(AuthCookieTestCase) + + +if __name__ == '__main__': + Rand.load_file('randpool.dat', -1) + unittest.TextTestRunner().run(suite()) + Rand.save_file('randpool.dat') + diff --git a/tests/test_bio.py b/tests/test_bio.py new file mode 100644 index 0000000..1d7b0c3 --- /dev/null +++ b/tests/test_bio.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python + +""" +Unit tests for M2Crypto.BIO. + +Copyright (c) 1999-2003 Ng Pheng Siong. All rights reserved. + +Copyright (c) 2006 Open Source Applications Foundation +Author: Heikki Toivonen +""" + +import unittest +from M2Crypto import BIO, Rand + +from fips import fips_mode + +class CipherStreamTestCase(unittest.TestCase): + def try_algo(self, algo): + enc = 1 + dec = 0 + data = '123456789012345678901234' + # Encrypt. + mem = BIO.MemoryBuffer() + cf = BIO.CipherStream(mem) + cf.set_cipher(algo, 'key', 'iv', 1) + cf.write(data) + cf.flush() + cf.write_close() + cf.close() + xxx = mem.read() + + # Decrypt. + mem = BIO.MemoryBuffer(xxx) + cf = BIO.CipherStream(mem) + cf.set_cipher(algo, 'key', 'iv', 0) + cf.write_close() + data2 = cf.read() + cf.close() + assert not cf.readable() + + self.assertRaises(IOError, cf.read) + self.assertRaises(IOError, cf.readline) + self.assertRaises(IOError, cf.readlines) + + assert data == data2, '%s algorithm cipher test failed' % algo + + def test_ciphers(self): + ciphers=[ + 'des_ede_ecb', 'des_ede_cbc', 'des_ede_cfb', 'des_ede_ofb', + 'des_ede3_ecb', 'des_ede3_cbc', 'des_ede3_cfb', 'des_ede3_ofb', + 'aes_128_ecb', 'aes_128_cbc', 'aes_128_cfb', 'aes_128_ofb', + 'aes_192_ecb', 'aes_192_cbc', 'aes_192_cfb', 'aes_192_ofb', + 'aes_256_ecb', 'aes_256_cbc', 'aes_256_cfb', 'aes_256_ofb'] + nonfips_ciphers=['bf_ecb', 'bf_cbc', 'bf_cfb', 'bf_ofb', + #'idea_ecb', 'idea_cbc', 'idea_cfb', 'idea_ofb', + 'cast5_ecb', 'cast5_cbc', 'cast5_cfb', 'cast5_ofb', + #'rc5_ecb', 'rc5_cbc', 'rc5_cfb', 'rc5_ofb', + 'des_ecb', 'des_cbc', 'des_cfb', 'des_ofb', + 'rc4', 'rc2_40_cbc'] + if not fips_mode: # Forbidden ciphers + ciphers += nonfips_ciphers + for i in ciphers: + self.try_algo(i) + + self.assertRaises(ValueError, self.try_algo, 'nosuchalgo4567') + +def suite(): + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(CipherStreamTestCase)) + return suite + +if __name__ == '__main__': + Rand.load_file('randpool.dat', -1) + unittest.TextTestRunner().run(suite()) + Rand.save_file('randpool.dat') + diff --git a/tests/test_bio_file.py b/tests/test_bio_file.py new file mode 100644 index 0000000..e118386 --- /dev/null +++ b/tests/test_bio_file.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python + +"""Unit tests for M2Crypto.BIO.File. + +Copyright (c) 1999-2002 Ng Pheng Siong. All rights reserved.""" + +import unittest +import M2Crypto +from M2Crypto.BIO import File, openfile +import os, sys + +class FileTestCase(unittest.TestCase): + + def setUp(self): + self.data = 'abcdef' * 64 + if sys.platform != 'win32': + self.fname = os.tmpnam() + else: + import tempfile + self.fname = tempfile.mktemp() + + def tearDown(self): + try: + os.unlink(self.fname) + except OSError: + pass + + def test_openfile_rb(self): + # First create the file using Python's open(). + f = open(self.fname, 'wb') + f.write(self.data) + f.close() + # Now open the file using M2Crypto.BIO.openfile(). + f = openfile(self.fname, 'rb') + data = f.read(len(self.data)) + assert data == self.data + + def test_openfile_wb(self): + # First create the file using M2Crypto.BIO.openfile(). + f = openfile(self.fname, 'wb') + f.write(self.data) + f.close() + # Now open the file using Python's open(). + f = open(self.fname, 'rb') + data = f.read(len(self.data)) + assert data == self.data + + def test_closed(self): + f = openfile(self.fname, 'wb') + f.write(self.data) + f.close() + self.assertRaises(IOError, f.write, self.data) + + def test_use_pyfile(self): + # First create the file. + f = open(self.fname, 'wb') + f2 = File(f) + f2.write(self.data) + f2.close() + # Now read the file. + f = open(self.fname, 'rb') + data = f.read(len(self.data)) + assert data == self.data + + +def suite(): + # Python 2.2 warns that os.tmpnam() is unsafe. + try: + import warnings + warnings.filterwarnings('ignore') + except ImportError: + pass + return unittest.makeSuite(FileTestCase) + + +if __name__ == '__main__': + unittest.TextTestRunner().run(suite()) + diff --git a/tests/test_bio_iobuf.py b/tests/test_bio_iobuf.py new file mode 100644 index 0000000..358e5df --- /dev/null +++ b/tests/test_bio_iobuf.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python + +"""Unit tests for M2Crypto.BIO.IOBuffer. + +Copyright (c) 2000 Ng Pheng Siong. All rights reserved.""" + +from cStringIO import StringIO + +import unittest +import M2Crypto +from M2Crypto.BIO import IOBuffer, MemoryBuffer + +class IOBufferTestCase(unittest.TestCase): + + def setUp(self): + self._data = 'abcdef\n' + self.data = self._data * 1024 + + def tearDown(self): + pass + + def test_init_empty(self): + mb = MemoryBuffer() + io = IOBuffer(mb) + out = io.read() + assert out == '' + + def test_init_something(self): + mb = MemoryBuffer(self.data) + io = IOBuffer(mb) + out = io.read(len(self.data)) + assert out == self.data + + def test_read_less_than(self): + chunk = len(self.data) - 7 + mb = MemoryBuffer(self.data) + io = IOBuffer(mb) + out = io.read(chunk) + assert out == self.data[:chunk] + + def test_read_more_than(self): + chunk = len(self.data) + 8 + mb = MemoryBuffer(self.data) + io = IOBuffer(mb) + out = io.read(chunk) + assert out == self.data + + def test_readline(self): + buf = StringIO() + mb = MemoryBuffer(self.data) + io = IOBuffer(mb) + while 1: + out = io.readline() + if not out: + break + buf.write(out) + assert out == self._data + assert buf.getvalue() == self.data + + def test_readlines(self): + buf = StringIO() + mb = MemoryBuffer(self.data) + io = IOBuffer(mb) + lines = io.readlines() + for line in lines: + assert line == self._data + buf.write(line) + assert buf.getvalue() == self.data + + def test_closed(self): + mb = MemoryBuffer(self.data) + io = IOBuffer(mb) + io.close() + self.assertRaises(IOError, io.write, self.data) + assert not io.readable() and not io.writeable() + + def test_read_only(self): + mb = MemoryBuffer(self.data) + io = IOBuffer(mb, mode='r') + self.assertRaises(IOError, io.write, self.data) + assert not io.writeable() + + +def suite(): + return unittest.makeSuite(IOBufferTestCase) + + +if __name__ == '__main__': + unittest.TextTestRunner().run(suite()) + diff --git a/tests/test_bio_membuf.py b/tests/test_bio_membuf.py new file mode 100644 index 0000000..7d719fd --- /dev/null +++ b/tests/test_bio_membuf.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python + +"""Unit tests for M2Crypto.BIO.MemoryBuffer. + +Copyright (c) 2000 Ng Pheng Siong. All rights reserved.""" + +import unittest +import M2Crypto +from M2Crypto.BIO import MemoryBuffer + +class MemoryBufferTestCase(unittest.TestCase): + + def setUp(self): + self.data = 'abcdef' * 64 + + def tearDown(self): + pass + + def test_init_empty(self): + mb = MemoryBuffer() + assert len(mb) == 0 + out = mb.read() + assert out is None + + def test_init_something(self): + mb = MemoryBuffer(self.data) + assert len(mb) == len(self.data) + out = mb.read() + assert out == self.data + + def test_read_less_than(self): + chunk = len(self.data) - 7 + mb = MemoryBuffer(self.data) + out = mb.read(chunk) + assert out == self.data[:chunk] and len(mb) == (len(self.data) - chunk) + + def test_read_more_than(self): + chunk = len(self.data) + 8 + mb = MemoryBuffer(self.data) + out = mb.read(chunk) + assert out == self.data and len(mb) == 0 + + def test_write_close(self): + mb = MemoryBuffer(self.data) + assert mb.writeable() + mb.write_close() + assert mb.readable() + self.assertRaises(IOError, mb.write, self.data) + assert not mb.writeable() + + def test_closed(self): + mb = MemoryBuffer(self.data) + mb.close() + self.assertRaises(IOError, mb.write, self.data) + assert mb.readable() and not mb.writeable() + + +def suite(): + return unittest.makeSuite(MemoryBufferTestCase) + + +if __name__ == '__main__': + unittest.TextTestRunner().run(suite()) + diff --git a/tests/test_bio_ssl.py b/tests/test_bio_ssl.py new file mode 100644 index 0000000..0dee9da --- /dev/null +++ b/tests/test_bio_ssl.py @@ -0,0 +1,150 @@ +#!/usr/bin/env python +"""Unit tests for M2Crypto.BIO.File. + +Copyright (c) 1999-2002 Ng Pheng Siong. All rights reserved.""" + +import unittest, threading, sys, socket + +from M2Crypto import BIO +from M2Crypto import SSL +from M2Crypto import Err +from M2Crypto import Rand +from M2Crypto import threading as m2threading + +from test_ssl import srv_host, srv_port + +class HandshakeClient(threading.Thread): + + def __init__(self, host, port): + threading.Thread.__init__(self) + self.host = host + self.port = port + + def run(self): + ctx = SSL.Context() + ctx.load_cert_chain("tests/server.pem") + conn = SSL.Connection(ctx) + cipher_list = conn.get_cipher_list() + sslbio = BIO.SSLBio() + readbio = BIO.MemoryBuffer() + writebio = BIO.MemoryBuffer() + sslbio.set_ssl(conn) + conn.set_bio(readbio, writebio) + conn.set_connect_state() + sock = socket.socket() + sock.connect((self.host, self.port)) + + handshake_complete = False + while not handshake_complete: + ret = sslbio.do_handshake() + if ret <= 0: + if not sslbio.should_retry() or not sslbio.should_read(): + err_string = Err.get_error() + print err_string + sys.exit("unrecoverable error in handshake - client") + else: + output_token = writebio.read() + if output_token is not None: + sock.sendall(output_token) + else: + input_token = sock.recv(1024) + readbio.write(input_token) + else: + handshake_complete = True + + sock.close() + + +class SSLTestCase(unittest.TestCase): + + def setUp(self): + self.sslbio = BIO.SSLBio() + + def test_pass(self): # XXX leaks 64/24 bytes + pass + + def test_set_ssl(self): # XXX leaks 64/1312 bytes + ctx = SSL.Context() + conn = SSL.Connection(ctx) + self.sslbio.set_ssl(conn) + + def test_do_handshake_fail(self): # XXX leaks 64/42066 bytes + ctx = SSL.Context() + conn = SSL.Connection(ctx) + conn.set_connect_state() + self.sslbio.set_ssl(conn) + ret = self.sslbio.do_handshake() + assert ret == 0 + + def test_should_retry_fail(self): # XXX leaks 64/1312 bytes + ctx = SSL.Context() + conn = SSL.Connection(ctx) + self.sslbio.set_ssl(conn) + ret = self.sslbio.do_handshake() + assert ret == -1 + ret = self.sslbio.should_retry() + assert ret == 0 + + def test_should_write_fail(self): # XXX leaks 64/1312 bytes + ctx = SSL.Context() + conn = SSL.Connection(ctx) + self.sslbio.set_ssl(conn) + ret = self.sslbio.do_handshake() + assert ret == -1 + ret = self.sslbio.should_write() + assert ret == 0 + + def test_should_read_fail(self): # XXX leaks 64/1312 bytes + ctx = SSL.Context() + conn = SSL.Connection(ctx) + self.sslbio.set_ssl(conn) + ret = self.sslbio.do_handshake() + assert ret == -1 + ret = self.sslbio.should_read() + assert ret == 0 + + def test_do_handshake_succeed(self): # XXX leaks 196/26586 bytes + ctx = SSL.Context() + ctx.load_cert_chain("tests/server.pem") + conn = SSL.Connection(ctx) + self.sslbio.set_ssl(conn) + readbio = BIO.MemoryBuffer() + writebio = BIO.MemoryBuffer() + conn.set_bio(readbio, writebio) + conn.set_accept_state() + handshake_complete = False + sock = socket.socket() + sock.bind((srv_host, srv_port)) + sock.listen(5) + handshake_client = HandshakeClient(srv_host, srv_port) + handshake_client.start() + new_sock, addr = sock.accept() + while not handshake_complete: + input_token = new_sock.recv(1024) + readbio.write(input_token) + + ret = self.sslbio.do_handshake() + if ret <= 0: + if not self.sslbio.should_retry() or not self.sslbio.should_read(): + sys.exit("unrecoverable error in handshake - server") + else: + handshake_complete = True + + output_token = writebio.read() + if output_token is not None: + new_sock.sendall(output_token) + + handshake_client.join() + sock.close() + new_sock.close() + +def suite(): + return unittest.makeSuite(SSLTestCase) + + +if __name__ == '__main__': + Rand.load_file('randpool.dat', -1) + m2threading.init() + unittest.TextTestRunner().run(suite()) + m2threading.cleanup() + Rand.save_file('randpool.dat') diff --git a/tests/test_bn.py b/tests/test_bn.py new file mode 100755 index 0000000..62c83fe --- /dev/null +++ b/tests/test_bn.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python + +""" +Unit tests for M2Crypto.BN. + +Copyright (c) 2005 Open Source Applications Foundation. All rights reserved. +""" + +import unittest, re +from M2Crypto import BN, Rand + +loops = 16 + +class BNTestCase(unittest.TestCase): + + def test_rand(self): + # defaults + for x in range(loops): + r8 = BN.rand(8) + + # top + for x in range(loops): + r8 = BN.rand(8, top=0) + assert r8 & 128 + for x in range(loops): + r8 = BN.rand(8, top=1) + assert r8 & 192 + + # bottom + for x in range(loops): + r8 = BN.rand(8, bottom=1) + assert r8 % 2 == 1 + + # make sure we can get big numbers and work with them + for x in range(loops): + r8 = BN.rand(8, top=0) + r16 = BN.rand(16, top=0) + r32 = BN.rand(32, top=0) + r64 = BN.rand(64, top=0) + r128 = BN.rand(128, top=0) + r256 = BN.rand(256, top=0) + r512 = BN.rand(512, top=0) + assert r8 < r16 < r32 < r64 < r128 < r256 < r512 < (r512 + 1) + + + def test_rand_range(self): + # small range + for x in range(loops): + r = BN.rand_range(1) + assert r == 0 + + for x in range(loops): + r = BN.rand_range(4) + assert 0 <= r < 4 + + # large range + r512 = BN.rand(512, top=0) + for x in range(loops): + r = BN.rand_range(r512) + assert 0 <= r < r512 + + + def test_randfname(self): + m = re.compile('^[a-zA-Z0-9]{8}$') + for x in range(loops): + r = BN.randfname(8) + assert m.match(r) + + +def suite(): + return unittest.makeSuite(BNTestCase) + + +if __name__ == '__main__': + Rand.load_file('randpool.dat', -1) + unittest.TextTestRunner().run(suite()) + Rand.save_file('randpool.dat') + diff --git a/tests/test_dh.py b/tests/test_dh.py new file mode 100644 index 0000000..0e66c89 --- /dev/null +++ b/tests/test_dh.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python + +"""Unit tests for M2Crypto.DH. + +Copyright (c) 2000 Ng Pheng Siong. All rights reserved.""" + +import unittest +from M2Crypto import DH, BIO, Rand, m2 + +class DHTestCase(unittest.TestCase): + + params = 'tests/dhparam.pem' + + def genparam_callback(self, *args): + pass + + def genparam_callback2(self): + pass + + def test_init_junk(self): + self.assertRaises(TypeError, DH.DH, 'junk') + + def test_gen_params(self): + a = DH.gen_params(1024, 2, self.genparam_callback) + assert a.check_params() == 0 + + def test_gen_params_bad_cb(self): + a = DH.gen_params(1024, 2, self.genparam_callback2) + assert a.check_params() == 0 + + def test_print_params(self): + a = DH.gen_params(1024, 2, self.genparam_callback) + bio = BIO.MemoryBuffer() + a.print_params(bio) + params = bio.read() + assert params.find('(1024 bit)') + assert params.find('generator: 2 (0x2)') + + def test_load_params(self): + a = DH.load_params('tests/dhparams.pem') + assert a.check_params() == 0 + + def test_compute_key(self): + a = DH.load_params('tests/dhparams.pem') + b = DH.set_params(a.p, a.g) + a.gen_key() + b.gen_key() + ak = a.compute_key(b.pub) + bk = b.compute_key(a.pub) + assert ak == bk + self.assertEqual(len(a), 128) + + self.assertRaises(DH.DHError, setattr, a, 'p', 1) + self.assertRaises(DH.DHError, setattr, a, 'priv', 1) + + +def suite(): + return unittest.makeSuite(DHTestCase) + + +if __name__=='__main__': + Rand.load_file('randpool.dat', -1) + unittest.TextTestRunner().run(suite()) + Rand.save_file('randpool.dat') + diff --git a/tests/test_dsa.py b/tests/test_dsa.py new file mode 100644 index 0000000..7823f50 --- /dev/null +++ b/tests/test_dsa.py @@ -0,0 +1,98 @@ +#!/usr/bin/env python + +"""Unit tests for M2Crypto.DSA. + +Copyright (c) 2000 Ng Pheng Siong. All rights reserved.""" + +import unittest +import sha +from M2Crypto import DSA, BIO, Rand, m2 + +class DSATestCase(unittest.TestCase): + + errkey = 'tests/rsa.priv.pem' + privkey = 'tests/dsa.priv.pem' + pubkey = 'tests/dsa.pub.pem' + param = 'tests/dsa.param.pem' + + data = sha.sha('Can you spell subliminal channel?').digest() + different_data = sha.sha('I can spell.').digest() + + def callback(self, *args): + pass + + def test_loadkey_junk(self): + self.assertRaises(DSA.DSAError, DSA.load_key, self.errkey) + + def test_loadkey(self): + dsa = DSA.load_key(self.privkey) + assert len(dsa) == 1024 + self.assertRaises(AttributeError, getattr, dsa, 'foobar') + for k in ('p', 'q', 'g', 'priv', 'pub'): + self.assertRaises(DSA.DSAError, setattr, dsa, k, 1) + + def test_loadparam(self): + self.assertRaises(DSA.DSAError, DSA.load_key, self.param) + dsa = DSA.load_params(self.param) + assert not dsa.check_key() + assert len(dsa) == 1024 + + def test_sign(self): + dsa = DSA.load_key(self.privkey) + assert dsa.check_key() + r, s = dsa.sign(self.data) + assert dsa.verify(self.data, r, s) + assert not dsa.verify(self.data, s, r) + + def test_sign_asn1(self): + dsa = DSA.load_key(self.privkey) + blob = dsa.sign_asn1(self.data) + assert dsa.verify_asn1(self.data, blob) + + def test_sign_with_params_only(self): + dsa = DSA.load_params(self.param) + self.assertRaises(AssertionError, dsa.sign, self.data) + self.assertRaises(AssertionError, dsa.sign_asn1, self.data) + + def test_pub_verify(self): + dsa = DSA.load_key(self.privkey) + r, s = dsa.sign(self.data) + dsapub = DSA.load_pub_key(self.pubkey) + assert dsapub.check_key() + assert dsapub.verify(self.data, r, s) + self.assertRaises(DSA.DSAError, dsapub.sign) + + def test_verify_fail(self): + dsa = DSA.load_key(self.privkey) + r, s = dsa.sign(self.data) + assert not dsa.verify(self.different_data, r, s) + + def test_verify_fail2(self): + dsa = DSA.load_key(self.privkey) + r,s = dsa.sign(self.data) + dsa2 = DSA.load_params(self.param) + assert not dsa2.check_key() + self.assertRaises(AssertionError, dsa2.verify, self.data, r, s) + + def test_genparam_setparam_genkey(self): + dsa = DSA.gen_params(1024, self.callback) + assert len(dsa) == 1024 + p = dsa.p + q = dsa.q + g = dsa.g + dsa2 = DSA.set_params(p,q,g) + assert not dsa2.check_key() + dsa2.gen_key() + assert dsa2.check_key() + r,s = dsa2.sign(self.data) + assert dsa2.verify(self.data, r, s) + +def suite(): + return unittest.makeSuite(DSATestCase) + + +if __name__ == '__main__': + Rand.load_file('randpool.dat', -1) + unittest.TextTestRunner().run(suite()) + Rand.save_file('randpool.dat') + diff --git a/tests/test_ec_curves.py b/tests/test_ec_curves.py new file mode 100644 index 0000000..d01f863 --- /dev/null +++ b/tests/test_ec_curves.py @@ -0,0 +1,159 @@ +#!/usr/bin/env python +# XXX memory leaks +""" + Unit tests for M2Crypto.EC, the curves + + There are several ways one could unittest elliptical curves + but we are going to only validate that we are using the + OpenSSL curve and that it works with ECDSA. We will assume + OpenSSL has validated the curves themselves. + + Also, some curves are shorter than a SHA-1 digest of 160 + bits. To keep the testing simple, we will take advantage + of ECDSA's ability to sign any digest length and create a + digset string of only 48 bits. Remember we are testing our + ability to access the curve, not ECDSA itself. + + Copyright (c) 2006 Larry Bugbee. All rights reserved. + +""" + +import unittest +#import sha +from M2Crypto import EC, Rand +from test_ecdsa import ECDSATestCase as ECDSATest + + +curves = [ + ('secp112r1', 112), + ('secp112r2', 112), + ('secp128r1', 128), + ('secp128r2', 128), + ('secp160k1', 160), + ('secp160r1', 160), + ('secp160r2', 160), + ('secp192k1', 192), + ('secp224k1', 224), + ('secp224r1', 224), + ('secp256k1', 256), + ('secp384r1', 384), + ('secp521r1', 521), + + ('sect113r1', 113), + ('sect113r2', 113), + ('sect131r1', 131), + ('sect131r2', 131), + ('sect163k1', 163), + ('sect163r1', 163), + ('sect163r2', 163), + ('sect193r1', 193), + ('sect193r2', 193), + ('sect233k1', 233), + ('sect233r1', 233), + ('sect239k1', 239), + ('sect283k1', 283), + ('sect283r1', 283), + ('sect409k1', 409), + ('sect409r1', 409), + ('sect571k1', 571), + ('sect571r1', 571), + + ('X9_62_prime192v1', 192), + ('X9_62_prime192v2', 192), + ('X9_62_prime192v3', 192), + ('X9_62_prime239v1', 239), + ('X9_62_prime239v2', 239), + ('X9_62_prime239v3', 239), + ('X9_62_prime256v1', 256), + + ('X9_62_c2pnb163v1', 163), + ('X9_62_c2pnb163v2', 163), + ('X9_62_c2pnb163v3', 163), + ('X9_62_c2pnb176v1', 176), + ('X9_62_c2tnb191v1', 191), + ('X9_62_c2tnb191v2', 191), + ('X9_62_c2tnb191v3', 191), + ('X9_62_c2pnb208w1', 208), + ('X9_62_c2tnb239v1', 239), + ('X9_62_c2tnb239v2', 239), + ('X9_62_c2tnb239v3', 239), + ('X9_62_c2pnb272w1', 272), + ('X9_62_c2pnb304w1', 304), + ('X9_62_c2tnb359v1', 359), + ('X9_62_c2pnb368w1', 368), + ('X9_62_c2tnb431r1', 431), + + ('wap_wsg_idm_ecid_wtls1', 113), + ('wap_wsg_idm_ecid_wtls3', 163), + ('wap_wsg_idm_ecid_wtls4', 113), + ('wap_wsg_idm_ecid_wtls5', 163), + ('wap_wsg_idm_ecid_wtls6', 112), + ('wap_wsg_idm_ecid_wtls7', 160), + ('wap_wsg_idm_ecid_wtls8', 112), + ('wap_wsg_idm_ecid_wtls9', 160), + ('wap_wsg_idm_ecid_wtls10', 233), + ('wap_wsg_idm_ecid_wtls11', 233), + ('wap_wsg_idm_ecid_wtls12', 224), +] + +# The following two curves, according to OpenSSL, have a +# "Questionable extension field!" and are not supported by +# the OpenSSL inverse function. ECError: no inverse. +# As such they cannot be used for signing. They might, +# however, be usable for encryption but that has not +# been tested. Until thir usefulness can be established, +# they are not supported at this time. +#curves2 = [ +# ('ipsec3', 155), +# ('ipsec4', 185), +#] + +class ECCurveTests(unittest.TestCase): + #data = sha.sha('Kilroy was here!').digest() # 160 bits + data = "digest" # keep short (48 bits) so lesser curves + # will work... ECDSA requires curve be + # equal or longer than digest + + def genkey(self, curveName, curveLen): + curve = getattr(EC, 'NID_'+curveName) + ec = EC.gen_params(curve) + assert len(ec) == curveLen + ec.gen_key() + assert ec.check_key(), 'check_key() failure for "%s"' % curveName + return ec + +# def check_ec_curves_genkey(self): +# for curveName, curveLen in curves2: +# self.genkey(curveName, curveLen) +# +# self.assertRaises(AttributeError, self.genkey, +# 'nosuchcurve', 1) + + def sign_verify_ecdsa(self, curveName, curveLen): + ec = self.genkey(curveName, curveLen) + r, s = ec.sign_dsa(self.data) + assert ec.verify_dsa(self.data, r, s) + assert not ec.verify_dsa(self.data, s, r) + + def test_ec_curves_ECDSA(self): + for curveName, curveLen in curves: + self.sign_verify_ecdsa(curveName, curveLen) + + self.assertRaises(AttributeError, self.sign_verify_ecdsa, + 'nosuchcurve', 1) + +# for curveName, curveLen in curves2: +# self.assertRaises(EC.ECError, self.sign_verify_ecdsa, +# curveName, curveLen) + +def suite(): + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(ECCurveTests)) + return suite + + +if __name__ == '__main__': + Rand.load_file('randpool.dat', -1) + unittest.TextTestRunner().run(suite()) + Rand.save_file('randpool.dat') + diff --git a/tests/test_ecdh.py b/tests/test_ecdh.py new file mode 100644 index 0000000..29581dc --- /dev/null +++ b/tests/test_ecdh.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python + +"""Unit tests for M2Crypto.EC, ECDH part. + +Copyright (c) 2000 Ng Pheng Siong. All rights reserved. +Portions copyright (c) 2005-2006 Vrije Universiteit Amsterdam. All rights reserved. +""" + + +import unittest +from M2Crypto import EC, BIO, Rand, m2 +import sys + +class ECDHTestCase(unittest.TestCase): + + privkey = 'tests/ec.priv.pem' + + def test_init_junk(self): + self.assertRaises(TypeError, EC.EC, 'junk') + + def test_compute_key(self): + a = EC.load_key(self.privkey) + b = EC.gen_params(EC.NID_sect233k1) + b.gen_key() + ak = a.compute_dh_key(b.pub()) + bk = b.compute_dh_key(a.pub()) + assert ak == bk + + def test_pubkey_from_der(self): + a = EC.gen_params(EC.NID_sect233k1) + a.gen_key() + b = EC.gen_params(EC.NID_sect233k1) + b.gen_key() + a_pub_der = a.pub().get_der() + a_pub = EC.pub_key_from_der(a_pub_der) + ak = a.compute_dh_key(b.pub()) + bk = b.compute_dh_key(a_pub) + assert ak == bk + + +def suite(): + return unittest.makeSuite(ECDHTestCase) + + +if __name__=='__main__': + Rand.load_file('randpool.dat', -1) + unittest.TextTestRunner().run(suite()) + Rand.save_file('randpool.dat') + diff --git a/tests/test_ecdsa.py b/tests/test_ecdsa.py new file mode 100644 index 0000000..74d9a82 --- /dev/null +++ b/tests/test_ecdsa.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python + +"""Unit tests for M2Crypto.EC, ECDSA part. + +Copyright (c) 2000 Ng Pheng Siong. All rights reserved. +Portions copyright (c) 2005-2006 Vrije Universiteit Amsterdam. All rights reserved. +""" + +import unittest +import sha +from M2Crypto import EC, BIO, Rand, m2 + +class ECDSATestCase(unittest.TestCase): + + errkey = 'tests/rsa.priv.pem' + privkey = 'tests/ec.priv.pem' + pubkey = 'tests/ec.pub.pem' + + data = sha.sha('Can you spell subliminal channel?').digest() + + def callback(self, *args): + pass + + def callback2(self): + pass + + def test_loadkey_junk(self): + self.assertRaises(ValueError, EC.load_key, self.errkey) + + def test_loadkey(self): + ec = EC.load_key(self.privkey) + assert len(ec) == 233 + + def test_loadpubkey(self): + # XXX more work needed + ec = EC.load_pub_key(self.pubkey) + assert len(ec) == 233 + self.assertRaises(EC.ECError, EC.load_pub_key, self.errkey) + + def _test_sign_dsa(self): + ec = EC.gen_params(EC.NID_sect233k1) + # ec.gen_key() + self.assertRaises(EC.ECError, ec.sign_dsa, self.data) + ec = EC.load_key(self.privkey) + r, s = ec.sign_dsa(self.data) + assert ec.verify_dsa(self.data, r, s) + assert not ec.verify_dsa(self.data, s, r) + + def test_sign_dsa_asn1(self): + ec = EC.load_key(self.privkey) + blob = ec.sign_dsa_asn1(self.data) + assert ec.verify_dsa_asn1(self.data, blob) + self.assertRaises(EC.ECError, ec.verify_dsa_asn1, blob, self.data) + + def test_verify_dsa(self): + ec = EC.load_key(self.privkey) + r, s = ec.sign_dsa(self.data) + ec2 = EC.load_pub_key(self.pubkey) + assert ec2.verify_dsa(self.data, r, s) + assert not ec2.verify_dsa(self.data, s, r) + + def test_genparam(self): + ec = EC.gen_params(EC.NID_sect233k1) + assert len(ec) == 233 + + +def suite(): + return unittest.makeSuite(ECDSATestCase) + + +if __name__ == '__main__': + Rand.load_file('randpool.dat', -1) + unittest.TextTestRunner().run(suite()) + Rand.save_file('randpool.dat') + diff --git a/tests/test_engine.py b/tests/test_engine.py new file mode 100644 index 0000000..91c2aa8 --- /dev/null +++ b/tests/test_engine.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python + +"""Unit tests for M2Crypto.Engine.""" + +import unittest +from M2Crypto import Engine, m2 + +class EngineTestCase(unittest.TestCase): + + privkey = 'tests/rsa.priv.pem' + bad_id = '1bea1edfeb97' + + def tearDown(self): + Engine.cleanup() + + def test_by_id_junk(self): + self.assertRaises(ValueError, Engine.Engine, self.bad_id) + self.assertRaises(ValueError, Engine.Engine) + + def test_by_id_openssl(self): + Engine.load_openssl() + e = Engine.Engine('openssl') + self.assertEqual(e.get_name(), 'Software engine support') + self.assertEqual(e.get_id(), 'openssl') + + def test_by_id_dynamic(self): + Engine.load_dynamic() + Engine.Engine('dynamic') + + def test_load_private(self): + Engine.load_openssl() + e = Engine.Engine('openssl') + e.set_default() + e.load_private_key(self.privkey) + + def test_load_certificate(self): + Engine.load_openssl() + e = Engine.Engine('openssl') + e.set_default() + self.assertRaises(Engine.EngineError, e.load_certificate, '/dev/null') + +def suite(): + return unittest.makeSuite(EngineTestCase) + + +if __name__ == '__main__': + unittest.TextTestRunner().run(suite()) + diff --git a/tests/test_evp.py b/tests/test_evp.py new file mode 100644 index 0000000..ba09092 --- /dev/null +++ b/tests/test_evp.py @@ -0,0 +1,471 @@ +#!/usr/bin/env python + +""" +Unit tests for M2Crypto.EVP. + +Copyright (c) 2004-2007 Open Source Applications Foundation +Author: Heikki Toivonen +""" + +import unittest +import cStringIO, sha +from binascii import hexlify, unhexlify +from M2Crypto import EVP, RSA, util, Rand, m2, BIO +from M2Crypto.util import h2b + +from fips import fips_mode + +class EVPTestCase(unittest.TestCase): + def _gen_callback(self, *args): + pass + + def _pass_callback(self, *args): + return 'foobar' + + def _assign_rsa(self): + rsa = RSA.gen_key(1024, 3, callback=self._gen_callback) + pkey = EVP.PKey() + pkey.assign_rsa(rsa, capture=0) # capture=1 should cause crash + return rsa + + def test_assign(self): + rsa = self._assign_rsa() + rsa.check_key() + + def test_pem(self): + rsa = RSA.gen_key(1024, 3, callback=self._gen_callback) + pkey = EVP.PKey() + pkey.assign_rsa(rsa) + assert pkey.as_pem(callback=self._pass_callback) != pkey.as_pem(cipher=None) + self.assertRaises(ValueError, pkey.as_pem, cipher='noXX$$%%suchcipher', + callback=self._pass_callback) + + def test_as_der(self): + """ + Test DER encoding the PKey instance after assigning + a RSA key to it. + """ + rsa = RSA.gen_key(1024, 3, callback=self._gen_callback) + pkey = EVP.PKey() + pkey.assign_rsa(rsa) + der_blob = pkey.as_der() + #A quick but not thorough sanity check + assert len(der_blob) == 160 + + + def test_MessageDigest(self): + self.assertRaises(ValueError, EVP.MessageDigest, 'sha513') + md = EVP.MessageDigest('sha1') + assert md.update('Hello') == 1 + assert util.octx_to_num(md.final()) == 1415821221623963719413415453263690387336440359920 + + def test_as_der_capture_key(self): + """ + Test DER encoding the PKey instance after assigning + a RSA key to it. Have the PKey instance capture the RSA key. + """ + rsa = RSA.gen_key(1024, 3, callback=self._gen_callback) + pkey = EVP.PKey() + pkey.assign_rsa(rsa, 1) + der_blob = pkey.as_der() + #A quick but not thorough sanity check + assert len(der_blob) == 160 + + def test_size(self): + rsa = RSA.gen_key(1024, 3, callback=self._gen_callback) + pkey = EVP.PKey() + pkey.assign_rsa(rsa) + size = pkey.size() + assert size == 128 + + def test_hmac(self): + assert util.octx_to_num(EVP.hmac('key', 'data')) == 92800611269186718152770431077867383126636491933, util.octx_to_num(EVP.hmac('key', 'data')) + if not fips_mode: # Disabled algorithms + assert util.octx_to_num(EVP.hmac('key', 'data', algo='md5')) == 209168838103121722341657216703105225176, util.octx_to_num(EVP.hmac('key', 'data', algo='md5')) + assert util.octx_to_num(EVP.hmac('key', 'data', algo='ripemd160')) == 1176807136224664126629105846386432860355826868536, util.octx_to_num(EVP.hmac('key', 'data', algo='ripemd160')) + + if m2.OPENSSL_VERSION_NUMBER >= 0x90800F: + assert util.octx_to_num(EVP.hmac('key', 'data', algo='sha224')) == 2660082265842109788381286338540662430962855478412025487066970872635, util.octx_to_num(EVP.hmac('key', 'data', algo='sha224')) + assert util.octx_to_num(EVP.hmac('key', 'data', algo='sha256')) == 36273358097036101702192658888336808701031275731906771612800928188662823394256, util.octx_to_num(EVP.hmac('key', 'data', algo='sha256')) + assert util.octx_to_num(EVP.hmac('key', 'data', algo='sha384')) == 30471069101236165765942696708481556386452105164815350204559050657318908408184002707969468421951222432574647369766282, util.octx_to_num(EVP.hmac('key', 'data', algo='sha384')) + assert util.octx_to_num(EVP.hmac('key', 'data', algo='sha512')) == 3160730054100700080556942280820129108466291087966635156623014063982211353635774277148932854680195471287740489442390820077884317620321797003323909388868696, util.octx_to_num(EVP.hmac('key', 'data', algo='sha512')) + + self.assertRaises(ValueError, EVP.hmac, 'key', 'data', algo='sha513') + + + def test_get_rsa(self): + """ + Testing retrieving the RSA key from the PKey instance. + """ + rsa = RSA.gen_key(512, 3, callback=self._gen_callback) + assert isinstance(rsa, RSA.RSA) + pkey = EVP.PKey() + pkey.assign_rsa(rsa) + rsa2 = pkey.get_rsa() + assert isinstance(rsa2, RSA.RSA_pub) + assert rsa.e == rsa2.e + assert rsa.n == rsa2.n + pem = rsa.as_pem(callback=self._pass_callback) + pem2 = rsa2.as_pem() + assert pem + assert pem2 + assert pem != pem2 + + message = "This is the message string" + digest = sha.sha(message).digest() + assert rsa.sign(digest) == rsa2.sign(digest) + + rsa3 = RSA.gen_key(1024, 3, callback=self._gen_callback) + assert rsa.sign(digest) != rsa3.sign(digest) + + def test_get_rsa_fail(self): + """ + Testing trying to retrieve the RSA key from the PKey instance + when it is not holding a RSA Key. Should raise a ValueError. + """ + pkey = EVP.PKey() + self.assertRaises(ValueError, pkey.get_rsa) + + def test_get_modulus(self): + pkey = EVP.PKey() + self.assertRaises(ValueError, pkey.get_modulus) + + rsa = RSA.gen_key(512, 3, callback=self._gen_callback) + pkey.assign_rsa(rsa) + mod = pkey.get_modulus() + assert len(mod) > 0, mod + assert len(mod.strip('0123456789ABCDEF')) == 0 + + def test_verify_final(self): + from M2Crypto import X509 + pkey = EVP.load_key('tests/signer_key.pem') + pkey.sign_init() + pkey.sign_update('test message') + sig = pkey.sign_final() + + # OK + x509 = X509.load_cert('tests/signer.pem') + pubkey = x509.get_pubkey() + pubkey.verify_init() + pubkey.verify_update('test message') + assert pubkey.verify_final(sig) == 1 + + # wrong cert + x509 = X509.load_cert('tests/x509.pem') + pubkey = x509.get_pubkey() + pubkey.verify_init() + pubkey.verify_update('test message') + assert pubkey.verify_final(sig) == 0 + + # wrong message + x509 = X509.load_cert('tests/signer.pem') + pubkey = x509.get_pubkey() + pubkey.verify_init() + pubkey.verify_update('test message not') + assert pubkey.verify_final(sig) == 0 + + def test_load_bad(self): + self.assertRaises(BIO.BIOError, EVP.load_key, + 'thisdoesnotexist-dfgh56789') + self.assertRaises(EVP.EVPError, EVP.load_key, + 'tests/signer.pem') # not a key + self.assertRaises(EVP.EVPError, EVP.load_key_bio, + BIO.MemoryBuffer('no a key')) + + def test_pad(self): + self.assertEqual(util.pkcs5_pad('Hello World'), + 'Hello World\x05\x05\x05\x05\x05') + self.assertEqual(util.pkcs7_pad('Hello World', 15), + 'Hello World\x04\x04\x04\x04') + self.assertRaises(ValueError, util.pkcs7_pad, 'Hello', 256) + + +class CipherTestCase(unittest.TestCase): + def cipher_filter(self, cipher, inf, outf): + while 1: + buf=inf.read() + if not buf: + break + outf.write(cipher.update(buf)) + outf.write(cipher.final()) + return outf.getvalue() + + def try_algo(self, algo): + enc = 1 + dec = 0 + otxt='against stupidity the gods themselves contend in vain' + + k=EVP.Cipher(algo, 'goethe','12345678', enc, 1, 'sha1', 'saltsalt', 5) + pbuf=cStringIO.StringIO(otxt) + cbuf=cStringIO.StringIO() + ctxt=self.cipher_filter(k, pbuf, cbuf) + pbuf.close() + cbuf.close() + + j=EVP.Cipher(algo, 'goethe','12345678', dec, 1, 'sha1', 'saltsalt', 5) + pbuf=cStringIO.StringIO() + cbuf=cStringIO.StringIO(ctxt) + ptxt=self.cipher_filter(j, cbuf, pbuf) + pbuf.close() + cbuf.close() + + assert otxt == ptxt, '%s algorithm cipher test failed' % algo + + def test_ciphers(self): + ciphers=[ + 'des_ede_ecb', 'des_ede_cbc', 'des_ede_cfb', 'des_ede_ofb', + 'des_ede3_ecb', 'des_ede3_cbc', 'des_ede3_cfb', 'des_ede3_ofb', + 'aes_128_ecb', 'aes_128_cbc', 'aes_128_cfb', 'aes_128_ofb', + 'aes_192_ecb', 'aes_192_cbc', 'aes_192_cfb', 'aes_192_ofb', + 'aes_256_ecb', 'aes_256_cbc', 'aes_256_cfb', 'aes_256_ofb'] + nonfips_ciphers=['bf_ecb', 'bf_cbc', 'bf_cfb', 'bf_ofb', + #'idea_ecb', 'idea_cbc', 'idea_cfb', 'idea_ofb', + 'cast5_ecb', 'cast5_cbc', 'cast5_cfb', 'cast5_ofb', + #'rc5_ecb', 'rc5_cbc', 'rc5_cfb', 'rc5_ofb', + 'des_ecb', 'des_cbc', 'des_cfb', 'des_ofb', + 'rc4', 'rc2_40_cbc'] + if not fips_mode: # Disabled algorithms + ciphers += nonfips_ciphers + for i in ciphers: + self.try_algo(i) + + # idea might not be compiled in + ciphers=['idea_ecb', 'idea_cbc', 'idea_cfb', 'idea_ofb'] + try: + for i in ciphers: + self.try_algo(i) + except ValueError, e: + if str(e) != "('unknown cipher', 'idea_ecb')": + raise + + # rc5 might not be compiled in + ciphers=['rc5_ecb', 'rc5_cbc', 'rc5_cfb', 'rc5_ofb'] + try: + for i in ciphers: + self.try_algo(i) + except ValueError, e: + if str(e) != "('unknown cipher', 'rc5_ecb')": + raise + + self.assertRaises(ValueError, self.try_algo, 'nosuchalgo4567') + + def test_AES(self): + enc = 1 + dec = 0 + tests = [ + # test vectors from rfc 3602 + #Case #1: Encrypting 16 bytes (1 block) using AES-CBC with 128-bit key + { + 'KEY': '06a9214036b8a15b512e03d534120006', + 'IV': '3dafba429d9eb430b422da802c9fac41', + 'PT': 'Single block msg', + 'CT': 'e353779c1079aeb82708942dbe77181a', + }, + + #Case #2: Encrypting 32 bytes (2 blocks) using AES-CBC with 128-bit key + { + 'KEY': 'c286696d887c9aa0611bbb3e2025a45a', + 'IV': '562e17996d093d28ddb3ba695a2e6f58', + 'PT': unhexlify('000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f'), + 'CT': 'd296cd94c2cccf8a3a863028b5e1dc0a7586602d253cfff91b8266bea6d61ab1', + }, + + #Case #3: Encrypting 48 bytes (3 blocks) using AES-CBC with 128-bit key + { + 'KEY': '6c3ea0477630ce21a2ce334aa746c2cd', + 'IV': 'c782dc4c098c66cbd9cd27d825682c81', + 'PT': 'This is a 48-byte message (exactly 3 AES blocks)', + 'CT': 'd0a02b3836451753d493665d33f0e8862dea54cdb293abc7506939276772f8d5021c19216bad525c8579695d83ba2684', + }, + ] + + # Test with padding + for test in tests: + # encrypt + k=EVP.Cipher(alg='aes_128_cbc', key=unhexlify(test['KEY']), iv=unhexlify(test['IV']), op=enc) + pbuf=cStringIO.StringIO(test['PT']) + cbuf=cStringIO.StringIO() + ciphertext = hexlify(self.cipher_filter(k, pbuf, cbuf)) + cipherpadding = ciphertext[len(test['PT']) * 2:] + ciphertext = ciphertext[:len(test['PT']) * 2] # Remove the padding from the end + pbuf.close() + cbuf.close() + self.assertEqual(ciphertext, test['CT']) + + # decrypt + j=EVP.Cipher(alg='aes_128_cbc', key=unhexlify(test['KEY']), iv=unhexlify(test['IV']), op=dec) + pbuf=cStringIO.StringIO() + cbuf=cStringIO.StringIO(unhexlify(test['CT'] + cipherpadding)) + plaintext=self.cipher_filter(j, cbuf, pbuf) + pbuf.close() + cbuf.close() + self.assertEqual(plaintext, test['PT']) + + # Test without padding + for test in tests: + # encrypt + k=EVP.Cipher(alg='aes_128_cbc', key=unhexlify(test['KEY']), iv=unhexlify(test['IV']), op=enc, padding=False) + pbuf=cStringIO.StringIO(test['PT']) + cbuf=cStringIO.StringIO() + ciphertext = hexlify(self.cipher_filter(k, pbuf, cbuf)) + pbuf.close() + cbuf.close() + self.assertEqual(ciphertext, test['CT']) + + # decrypt + j=EVP.Cipher(alg='aes_128_cbc', key=unhexlify(test['KEY']), iv=unhexlify(test['IV']), op=dec, padding=False) + pbuf=cStringIO.StringIO() + cbuf=cStringIO.StringIO(unhexlify(test['CT'])) + plaintext=self.cipher_filter(j, cbuf, pbuf) + pbuf.close() + cbuf.close() + self.assertEqual(plaintext, test['PT']) + + + def test_raises(self): + def _cipherFilter(cipher, inf, outf): + while 1: + buf = inf.read() + if not buf: + break + outf.write(cipher.update(buf)) + outf.write(cipher.final()) + return outf.getvalue() + + def decrypt(ciphertext, key, iv, alg='aes_256_cbc'): + cipher = EVP.Cipher(alg=alg, key=key, iv=iv, op=0) + pbuf = cStringIO.StringIO() + cbuf = cStringIO.StringIO(ciphertext) + plaintext = _cipherFilter(cipher, cbuf, pbuf) + pbuf.close() + cbuf.close() + return plaintext + + self.assertRaises(EVP.EVPError, decrypt, + unhexlify('941d3647a642fab26d9f99a195098b91252c652d07235b9db35758c401627711724637648e45cad0f1121751a1240a4134998cfdf3c4a95c72de2a2444de3f9e40d881d7f205630b0d8ce142fdaebd8d7fbab2aea3dc47f5f29a0e9b55aae59222671d8e2877e1fb5cd8ef1c427027e0'), + unhexlify('5f2cc54067f779f74d3cf1f78c735aec404c8c3a4aaaa02eb1946f595ea4cddb'), + unhexlify('0001efa4bd154ee415b9413a421cedf04359fff945a30e7c115465b1c780a85b65c0e45c')) + + self.assertRaises(EVP.EVPError, decrypt, + unhexlify('a78a510416c1a6f1b48077cc9eeb4287dcf8c5d3179ef80136c18876d774570d'), + unhexlify('5cd148eeaf680d4ff933aed83009cad4110162f53ef89fd44fad09611b0524d4'), + unhexlify('')) + + +class PBKDF2TestCase(unittest.TestCase): + def test_rfc3211_test_vectors(self): + from binascii import hexlify, unhexlify + + password = 'password' + salt = unhexlify('12 34 56 78 78 56 34 12'.replace(' ', '')) + iter = 5 + keylen = 8 + ret = EVP.pbkdf2(password, salt, iter, keylen) + self.assertEqual(hexlify(ret), 'D1 DA A7 86 15 F2 87 E6'.replace(' ', '').lower()) + + password = 'All n-entities must communicate with other n-entities via n-1 entiteeheehees' + salt = unhexlify('12 34 56 78 78 56 34 12'.replace(' ', '')) + iter = 500 + keylen = 16 + ret = EVP.pbkdf2(password, salt, iter, keylen) + self.assertEqual(hexlify(ret), '6A 89 70 BF 68 C9 2C AE A8 4A 8D F2 85 10 85 86'.replace(' ', '').lower()) + + +class HMACTestCase(unittest.TestCase): + data1=['', 'More text test vectors to stuff up EBCDIC machines :-)', \ + h2b("e9139d1e6ee064ef8cf514fc7dc83e86")] + + data2=[h2b('0b'*16), "Hi There", \ + h2b("9294727a3638bb1c13f48ef8158bfc9d")] + + data3=['Jefe', "what do ya want for nothing?", \ + h2b("750c783e6ab0b503eaa86e310a5db738")] + + data4=[h2b('aa'*16), h2b('dd'*50), \ + h2b("0x56be34521d144c88dbb8c733f0e8b3f6")] + + data=[data1, data2, data3, data4] + + def test_simple(self): + algo = 'md5' + for d in self.data: + h = EVP.HMAC(d[0], algo) + h.update(d[1]) + ret = h.final() + self.assertEqual(ret, d[2]) + self.assertRaises(ValueError, EVP.HMAC, d[0], algo='nosuchalgo') + + def make_chain_HMAC(self, key, start, input, algo='sha1'): + chain = [] + hmac = EVP.HMAC(key, algo) + hmac.update(`start`) + digest = hmac.final() + chain.append((digest, start)) + for i in input: + hmac.reset(digest) + hmac.update(`i`) + digest = hmac.final() + chain.append((digest, i)) + return chain + + def make_chain_hmac(self, key, start, input, algo='sha1'): + from M2Crypto.EVP import hmac + chain = [] + digest = hmac(key, `start`, algo) + chain.append((digest, start)) + for i in input: + digest = hmac(digest, `i`, algo) + chain.append((digest, i)) + return chain + + def verify_chain_hmac(self, key, start, chain, algo='sha1'): + from M2Crypto.EVP import hmac + digest = hmac(key, `start`, algo) + c = chain[0] + if c[0] != digest or c[1] != start: + return 0 + for d, v in chain[1:]: + digest = hmac(digest, `v`, algo) + if digest != d: + return 0 + return 1 + + def verify_chain_HMAC(self, key, start, chain, algo='sha1'): + hmac = EVP.HMAC(key, algo) + hmac.update(`start`) + digest = hmac.final() + c = chain[0] + if c[0] != digest or c[1] != start: + return 0 + for d, v in chain[1:]: + hmac.reset(digest) + hmac.update(`v`) + digest = hmac.final() + if digest != d: + return 0 + return 1 + + def test_complicated(self): + make_chain = self.make_chain_hmac + verify_chain = self.verify_chain_hmac + key = 'numero uno' + start = 'zeroth item' + input = ['first item', 'go go go', 'fly fly fly'] + chain = make_chain(key, start, input) + self.assertEquals(verify_chain('some key', start, chain), 0) + self.assertEquals(verify_chain(key, start, chain), 1) + + +def suite(): + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(EVPTestCase)) + suite.addTest(unittest.makeSuite(CipherTestCase)) + suite.addTest(unittest.makeSuite(PBKDF2TestCase)) + suite.addTest(unittest.makeSuite(HMACTestCase)) + return suite + +if __name__ == '__main__': + Rand.load_file('randpool.dat', -1) + unittest.TextTestRunner().run(suite()) + Rand.save_file('randpool.dat') + diff --git a/tests/test_obj.py b/tests/test_obj.py new file mode 100644 index 0000000..9144135 --- /dev/null +++ b/tests/test_obj.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python + +"""Unit tests for M2Crypto.m2 obj_* functions. +""" + +import unittest +from M2Crypto import X509, ASN1, BIO, Rand, m2 + +""" +These functions must be cleaned up and moved to some python module +Taken from CA managment code +""" + +def x509_name2list(name): + for i in range(0, name.entry_count()): + yield X509.X509_Name_Entry(m2.x509_name_get_entry(name._ptr(), i), _pyfree = 0) + +def x509_name_entry2tuple(entry): + bio = BIO.MemoryBuffer() + m2.asn1_string_print(bio._ptr(), m2.x509_name_entry_get_data(entry._ptr())) + return (m2.obj_obj2txt(m2.x509_name_entry_get_object(entry._ptr()), 0), bio.getvalue()) + +def tuple2x509_name_entry(tup): + obj, data = tup + _x509_ne = m2.x509_name_entry_create_by_txt(None, obj, ASN1.MBSTRING_ASC, data, len(data)) + if not _x509_ne: + raise ValueError("Invalid object indentifier: %s" % obj) + return X509.X509_Name_Entry(_x509_ne, _pyfree = 1) # Prevent memory leaks + +class ObjectsTestCase(unittest.TestCase): + + def callback(self, *args): + pass + + def test_obj2txt(self): + assert m2.obj_obj2txt(m2.obj_txt2obj("commonName", 0), 1) == "2.5.4.3", "2.5.4.3" + assert m2.obj_obj2txt(m2.obj_txt2obj("commonName", 0), 0) == "commonName", "commonName" + + def test_nid(self): + assert m2.obj_ln2nid("commonName") == m2.obj_txt2nid("2.5.4.3"), "ln2nid and txt2nid mismatch" + assert m2.obj_ln2nid("CN") == 0, "ln2nid on sn" + assert m2.obj_sn2nid("CN") == m2.obj_ln2nid("commonName"), "ln2nid and sn2nid mismatch" + assert m2.obj_sn2nid("CN") == m2.obj_obj2nid(m2.obj_txt2obj("CN", 0)), "obj2nid" + assert m2.obj_txt2nid("__unknown") == 0, "__unknown" + + def test_tuple2tuple(self): + tup = ("CN", "someCommonName") + tup1 = x509_name_entry2tuple(tuple2x509_name_entry(tup)) + assert tup1[1] == tup[1], tup1 # tup1[0] is 'commonName', not 'CN' + assert x509_name_entry2tuple(tuple2x509_name_entry(tup1)) == tup1, tup1 + + def test_unknown(self): + self.assertRaises(ValueError, tuple2x509_name_entry, ("__unknown", "_")) + + def test_x509_name(self): + n = X509.X509_Name() + n.C = 'US' # It seems this actually needs to be a real 2 letter country code + n.SP = 'State or Province' + n.L = 'locality name' + n.O = 'orhanization name' + n.OU = 'org unit' + n.CN = 'common name' + n.Email = 'bob@example.com' + n.serialNumber = '1234' + n.SN = 'surname' + n.GN = 'given name' + + n.givenName = 'name given' + assert len(n) == 11, len(n) + + tl = map(x509_name_entry2tuple, x509_name2list(n)) + + assert len(tl) == len(n), len(tl) + + x509_n = m2.x509_name_new() + for o in map(tuple2x509_name_entry, tl): + m2.x509_name_add_entry(x509_n, o._ptr(), -1, 0) + o._pyfree = 0 # Take care of underlying object + n1 = X509.X509_Name(x509_n) + + assert n.as_text() == n1.as_text(), n1.as_text() + +def suite(): + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(ObjectsTestCase)) + return suite + + +if __name__ == '__main__': + Rand.load_file('randpool.dat', -1) + unittest.TextTestRunner().run(suite()) + Rand.save_file('randpool.dat') diff --git a/tests/test_pgp.py b/tests/test_pgp.py new file mode 100644 index 0000000..c86d153 --- /dev/null +++ b/tests/test_pgp.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python + +"""PGP test program. + +Copyright (c) 1999 Ng Pheng Siong. All rights reserved.""" + +import unittest +from M2Crypto import EVP, PGP +from cStringIO import StringIO + + +class PGPTestCase(unittest.TestCase): + + def test_simple(self): + pkr = PGP.load_pubring('tests/pubring.pgp') + daft = pkr['daft'] + daft_pkt = daft._pubkey_pkt.pack() + s1 = EVP.MessageDigest('sha1') + s1.update(daft_pkt) + s1f = `s1.final()` + + buf = StringIO(daft_pkt) + ps = PGP.packet_stream(buf) + dift_pkt = ps.read() + s2 = EVP.MessageDigest('sha1') + s2.update(dift_pkt.pack()) + s2f = `s2.final()` + + self.assertEqual(s1f, s2f) + +def suite(): + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(PGPTestCase)) + return suite + + +if __name__ == '__main__': + Rand.load_file('randpool.dat', -1) + unittest.TextTestRunner().run(suite()) + Rand.save_file('randpool.dat') diff --git a/tests/test_rand.py b/tests/test_rand.py new file mode 100644 index 0000000..3e4d65d --- /dev/null +++ b/tests/test_rand.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python + +"""Unit tests for M2Crypto.Rand. + +Copyright (C) 2006 Open Source Applications Foundation (OSAF). All Rights Reserved. +""" + +import unittest +import os, sys +from M2Crypto import Rand + +class RandTestCase(unittest.TestCase): + def test_bytes(self): + self.assertRaises(MemoryError, Rand.rand_bytes, -1) + assert Rand.rand_bytes(0) == '' + assert len(Rand.rand_bytes(1)) == 1 + + def test_pseudo_bytes(self): + self.assertRaises(MemoryError, Rand.rand_pseudo_bytes, -1) + assert Rand.rand_pseudo_bytes(0) == ('', 1) + a, b = Rand.rand_pseudo_bytes(1) + assert len(a) == 1 + assert b == 1 + + def test_load_save(self): + try: + os.remove('tests/randpool.dat') + except OSError: + pass + assert Rand.load_file('tests/randpool.dat', -1) == 0 + assert Rand.save_file('tests/randpool.dat') == 1024 + assert Rand.load_file('tests/randpool.dat', -1) == 1024 + + def test_seed_add(self): + if sys.version_info >= (2, 4): + assert Rand.rand_seed(os.urandom(1024)) is None + + # XXX Should there be limits on the entropy parameter? + assert Rand.rand_add(os.urandom(2), 0.5) is None + Rand.rand_add(os.urandom(2), -0.5) + Rand.rand_add(os.urandom(2), 5000.0) + + +def suite(): + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(RandTestCase)) + return suite + + +if __name__ == '__main__': + unittest.TextTestRunner().run(suite()) diff --git a/tests/test_rc4.py b/tests/test_rc4.py new file mode 100644 index 0000000..5a2d239 --- /dev/null +++ b/tests/test_rc4.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python + +"""Unit tests for M2Crypto.RC4. + +Copyright (c) 2009 Heikki Toivonen. All rights reserved.""" + +import unittest +from binascii import hexlify +from M2Crypto import RC4 + +class RC4TestCase(unittest.TestCase): + + def test_vectors(self): + """ + Test with test vectors from Wikipedia: http://en.wikipedia.org/wiki/Rc4 + """ + vectors = (('Key', 'Plaintext', 'BBF316E8D940AF0AD3'), + ('Wiki', 'pedia', '1021BF0420'), + ('Secret', 'Attack at dawn', '45A01F645FC35B383552544B9BF5')) + + rc4 = RC4.RC4() + for key, plaintext, ciphertext in vectors: + rc4.set_key(key) + self.assertEqual(hexlify(rc4.update(plaintext)).upper(), ciphertext) + + self.assertEqual(rc4.final(), '') + + def test_bad(self): + rc4 = RC4.RC4('foo') + self.assertNotEqual(hexlify(rc4.update('bar')).upper(), '45678') + + +def suite(): + return unittest.makeSuite(RC4TestCase) + + +if __name__ == '__main__': + Rand.load_file('randpool.dat', -1) + unittest.TextTestRunner().run(suite()) + Rand.save_file('randpool.dat') + diff --git a/tests/test_rsa.py b/tests/test_rsa.py new file mode 100644 index 0000000..0bb986b --- /dev/null +++ b/tests/test_rsa.py @@ -0,0 +1,285 @@ +#!/usr/bin/env python + +"""Unit tests for M2Crypto.RSA. + +Copyright (c) 2000 Ng Pheng Siong. All rights reserved.""" + +import unittest +import sha, md5, os, sys +from M2Crypto import RSA, BIO, Rand, m2, EVP, X509 + +class RSATestCase(unittest.TestCase): + + errkey = 'tests/dsa.priv.pem' + privkey = 'tests/rsa.priv.pem' + privkey2 = 'tests/rsa.priv2.pem' + pubkey = 'tests/rsa.pub.pem' + + data = sha.sha('The magic words are squeamish ossifrage.').digest() + + e_padding_ok = ('pkcs1_padding', 'pkcs1_oaep_padding') + + s_padding_ok = ('pkcs1_padding',) + s_padding_nok = ('no_padding', 'sslv23_padding', 'pkcs1_oaep_padding') + + def gen_callback(self, *args): + pass + + def gen2_callback(self): + pass + + def pp_callback(self, *args): + # The passphrase for rsa.priv2.pem is 'qwerty'. + return 'qwerty' + + def pp2_callback(self, *args): + # Misbehaving passphrase callback. + pass + + def test_loadkey_junk(self): + self.assertRaises(RSA.RSAError, RSA.load_key, self.errkey) + + def test_loadkey_pp(self): + rsa = RSA.load_key(self.privkey2, self.pp_callback) + assert len(rsa) == 1024 + assert rsa.e == '\000\000\000\003\001\000\001' # aka 65537 aka 0xf4 + assert rsa.check_key() == 1 + + def test_loadkey_pp_bad_cb(self): + self.assertRaises(RSA.RSAError, RSA.load_key, self.privkey2, self.pp2_callback) + + def test_loadkey(self): + rsa = RSA.load_key(self.privkey) + assert len(rsa) == 1024 + assert rsa.e == '\000\000\000\003\001\000\001' # aka 65537 aka 0xf4 + self.assertEqual(rsa.n, "\x00\x00\x00\x81\x00\xcde!\x15\xdah\xb5`\xce[\xd6\x17d\xba8\xc1I\xb1\xf1\xber\x86K\xc7\xda\xb3\x98\xd6\xf6\x80\xae\xaa\x8f!\x9a\xefQ\xdeh\xbb\xc5\x99\x01o\xebGO\x8e\x9b\x9a\x18\xfb6\xba\x12\xfc\xf2\x17\r$\x00\xa1\x1a \xfc/\x13iUm\x04\x13\x0f\x91D~\xbf\x08\x19C\x1a\xe2\xa3\x91&\x8f\xcf\xcc\xf3\xa4HRf\xaf\xf2\x19\xbd\x05\xe36\x9a\xbbQ\xc86|(\xad\x83\xf2Eu\xb2EL\xdf\xa4@\x7f\xeel|\xfcU\x03\xdb\x89'") + self.assertRaises(AttributeError, getattr, rsa, 'nosuchprop') + assert rsa.check_key() == 1 + + def test_loadkey_bio(self): + keybio = BIO.MemoryBuffer(open(self.privkey).read()) + rsa = RSA.load_key_bio(keybio) + assert len(rsa) == 1024 + assert rsa.e == '\000\000\000\003\001\000\001' # aka 65537 aka 0xf4 + assert rsa.check_key() == 1 + + def test_keygen(self): + rsa = RSA.gen_key(1024, 65537, self.gen_callback) + assert len(rsa) == 1024 + assert rsa.e == '\000\000\000\003\001\000\001' # aka 65537 aka 0xf4 + assert rsa.check_key() == 1 + + def test_keygen_bad_cb(self): + rsa = RSA.gen_key(1024, 65537, self.gen2_callback) + assert len(rsa) == 1024 + assert rsa.e == '\000\000\000\003\001\000\001' # aka 65537 aka 0xf4 + assert rsa.check_key() == 1 + + def test_private_encrypt(self): + priv = RSA.load_key(self.privkey) + # pkcs1_padding + for padding in self.s_padding_ok: + p = getattr(RSA, padding) + ctxt = priv.private_encrypt(self.data, p) + ptxt = priv.public_decrypt(ctxt, p) + assert ptxt == self.data + # The other paddings. + for padding in self.s_padding_nok: + p = getattr(RSA, padding) + self.assertRaises(RSA.RSAError, priv.private_encrypt, self.data, p) + # Type-check the data to be encrypted. + self.assertRaises(TypeError, priv.private_encrypt, self.gen_callback, RSA.pkcs1_padding) + + def test_public_encrypt(self): + priv = RSA.load_key(self.privkey) + # pkcs1_padding, pkcs1_oaep_padding + for padding in self.e_padding_ok: + p = getattr(RSA, padding) + ctxt = priv.public_encrypt(self.data, p) + ptxt = priv.private_decrypt(ctxt, p) + assert ptxt == self.data + # sslv23_padding + ctxt = priv.public_encrypt(self.data, RSA.sslv23_padding) + self.assertRaises(RSA.RSAError, priv.private_decrypt, ctxt, RSA.sslv23_padding) + # no_padding + self.assertRaises(RSA.RSAError, priv.public_encrypt, self.data, RSA.no_padding) + # Type-check the data to be encrypted. + self.assertRaises(TypeError, priv.public_encrypt, self.gen_callback, RSA.pkcs1_padding) + + def test_x509_public_encrypt(self): + x509 = X509.load_cert("tests/recipient.pem") + rsa = x509.get_pubkey().get_rsa() + rsa.public_encrypt("data", RSA.pkcs1_padding) + + def test_loadpub(self): + rsa = RSA.load_pub_key(self.pubkey) + assert len(rsa) == 1024 + assert rsa.e == '\000\000\000\003\001\000\001' # aka 65537 aka 0xf4 + self.assertRaises(RSA.RSAError, setattr, rsa, 'e', '\000\000\000\003\001\000\001') + self.assertRaises(RSA.RSAError, rsa.private_encrypt, 1) + self.assertRaises(RSA.RSAError, rsa.private_decrypt, 1) + assert rsa.check_key() + + def test_loadpub_bad(self): + self.assertRaises(RSA.RSAError, RSA.load_pub_key, self.errkey) + + def test_savepub(self): + rsa = RSA.load_pub_key(self.pubkey) + assert rsa.as_pem() # calls save_key_bio + f = 'tests/rsa_test.pub' + try: + self.assertEquals(rsa.save_key(f), 1) + finally: + try: + os.remove(f) + except IOError: + pass + + def test_set_bn(self): + rsa = RSA.load_pub_key(self.pubkey) + assert m2.rsa_set_e(rsa.rsa, '\000\000\000\003\001\000\001') is None + self.assertRaises(RSA.RSAError, m2.rsa_set_e, rsa.rsa, '\000\000\000\003\001') + + def test_newpub(self): + old = RSA.load_pub_key(self.pubkey) + new = RSA.new_pub_key(old.pub()) + assert new.check_key() + assert len(new) == 1024 + assert new.e == '\000\000\000\003\001\000\001' # aka 65537 aka 0xf4 + + def test_sign_and_verify(self): + """ + Testing signing and verifying digests + """ + algos = {'sha1':'', + 'ripemd160':'', + 'md5':''} + + if m2.OPENSSL_VERSION_NUMBER >= 0x90800F: + algos['sha224'] = '' + algos['sha256'] = '' + algos['sha384'] = '' + algos['sha512'] = '' + + message = "This is the message string" + digest = sha.sha(message).digest() + rsa = RSA.load_key(self.privkey) + rsa2 = RSA.load_pub_key(self.pubkey) + for algo in algos.keys(): + signature = rsa.sign(digest, algo) + #assert signature == algos[algo], 'mismatched signature with algorithm %s: signature=%s' % (algo, signature) + verify = rsa2.verify(digest, signature, algo) + assert verify == 1, 'verification failed with algorithm %s' % algo + + if m2.OPENSSL_VERSION_NUMBER >= 0x90708F: + def test_sign_and_verify_rsassa_pss(self): + """ + Testing signing and verifying using rsassa_pss + + The maximum size of the salt has to decrease as the + size of the digest increases because of the size of + our test key limits it. + """ + message = "This is the message string" + if sys.version_info < (2, 5): + algos = {'sha1': (43, sha.sha(message).digest()), + 'md5': (47, md5.md5(message).digest())} + + else: + import hashlib + algos = {'sha1': 43, + 'ripemd160': 43, + 'md5': 47} + + if m2.OPENSSL_VERSION_NUMBER >= 0x90800F: + algos['sha224'] = 35 + algos['sha256'] = 31 + algos['sha384'] = 15 + algos['sha512'] = 0 + + for algo, salt_max in algos.iteritems(): + h = hashlib.new(algo) + h.update(message) + digest = h.digest() + algos[algo] = (salt_max, digest) + + rsa = RSA.load_key(self.privkey) + rsa2 = RSA.load_pub_key(self.pubkey) + for algo, (salt_max, digest) in algos.iteritems(): + for salt_length in range(0, salt_max): + signature = rsa.sign_rsassa_pss(digest, algo, salt_length) + verify = rsa2.verify_rsassa_pss(digest, signature, algo, salt_length) + assert verify == 1, 'verification failed with algorithm %s salt length %d' % (algo, salt_length) + + def test_sign_bad_method(self): + """ + Testing calling sign with an unsupported message digest algorithm + """ + rsa = RSA.load_key(self.privkey) + message = "This is the message string" + digest = md5.md5(message).digest() + self.assertRaises(ValueError, rsa.sign, + digest, 'bad_digest_method') + + def test_verify_bad_method(self): + """ + Testing calling verify with an unsupported message digest algorithm + """ + rsa = RSA.load_key(self.privkey) + message = "This is the message string" + digest = md5.md5(message).digest() + signature = rsa.sign(digest, 'sha1') + self.assertRaises(ValueError, rsa.verify, + digest, signature, 'bad_digest_method') + + def test_verify_mismatched_algo(self): + """ + Testing verify to make sure it fails when we use a different + message digest algorithm + """ + rsa = RSA.load_key(self.privkey) + message = "This is the message string" + digest = sha.sha(message).digest() + signature = rsa.sign(digest, 'sha1') + rsa2 = RSA.load_pub_key(self.pubkey) + self.assertRaises(RSA.RSAError, rsa.verify, + digest, signature, 'md5') + + def test_sign_fail(self): + """ + Testing sign to make sure it fails when I give it + a bogus digest. Looking at the RSA sign method + I discovered that with the digest methods we use + it has to be longer than a certain length. + """ + rsa = RSA.load_key(self.privkey) + digest = """This string should be long enough to warrant an error in + RSA_sign""" * 2 + + self.assertRaises(RSA.RSAError, rsa.sign, digest) + + def test_verify_bad_signature(self): + """ + Testing verify to make sure it fails when we use a bad signature + """ + rsa = RSA.load_key(self.privkey) + message = "This is the message string" + digest = sha.sha(message).digest() + + otherMessage = "Abracadabra" + otherDigest = sha.sha(otherMessage).digest() + otherSignature = rsa.sign(otherDigest) + + self.assertRaises(RSA.RSAError, rsa.verify, + digest, otherSignature) + + +def suite(): + return unittest.makeSuite(RSATestCase) + + +if __name__ == '__main__': + Rand.load_file('randpool.dat', -1) + unittest.TextTestRunner().run(suite()) + Rand.save_file('randpool.dat') + diff --git a/tests/test_smime.py b/tests/test_smime.py new file mode 100644 index 0000000..f18c9db --- /dev/null +++ b/tests/test_smime.py @@ -0,0 +1,262 @@ +#!/usr/bin/env python + +"""Unit tests for M2Crypto.SMIME. + +Copyright (C) 2006 Open Source Applications Foundation. All Rights Reserved. +""" + +import unittest +from M2Crypto import SMIME, BIO, Rand, X509, EVP + +class SMIMETestCase(unittest.TestCase): + cleartext = 'some text to manipulate' + + def setUp(self): + # XXX Ugly, but not sure what would be better + self.signed = self.test_sign() + self.encrypted = self.test_encrypt() + + def test_load_bad(self): + s = SMIME.SMIME() + self.assertRaises(EVP.EVPError, s.load_key, + 'tests/signer.pem', + 'tests/signer.pem') + + self.assertRaises(BIO.BIOError, SMIME.load_pkcs7, 'nosuchfile-dfg456') + self.assertRaises(SMIME.PKCS7_Error, SMIME.load_pkcs7, 'tests/signer.pem') + self.assertRaises(SMIME.PKCS7_Error, SMIME.load_pkcs7_bio, BIO.MemoryBuffer('no pkcs7')) + + self.assertRaises(SMIME.SMIME_Error, SMIME.smime_load_pkcs7, 'tests/signer.pem') + self.assertRaises(SMIME.SMIME_Error, SMIME.smime_load_pkcs7_bio, BIO.MemoryBuffer('no pkcs7')) + + def test_crlf(self): + self.assertEqual(SMIME.text_crlf('foobar'), 'Content-Type: text/plain\r\n\r\nfoobar') + self.assertEqual(SMIME.text_crlf_bio(BIO.MemoryBuffer('foobar')).read(), 'Content-Type: text/plain\r\n\r\nfoobar') + + def test_sign(self): + buf = BIO.MemoryBuffer(self.cleartext) + s = SMIME.SMIME() + s.load_key('tests/signer_key.pem', 'tests/signer.pem') + p7 = s.sign(buf, SMIME.PKCS7_DETACHED) + assert len(buf) == 0 + assert p7.type() == SMIME.PKCS7_SIGNED, p7.type() + assert isinstance(p7, SMIME.PKCS7), p7 + out = BIO.MemoryBuffer() + p7.write(out) + + buf = out.read() + + assert buf[:len('-----BEGIN PKCS7-----')] == '-----BEGIN PKCS7-----' + buf = buf.strip() + assert buf[-len('-----END PKCS7-----'):] == '-----END PKCS7-----', buf[-len('-----END PKCS7-----'):] + assert len(buf) > len('-----END PKCS7-----') + len('-----BEGIN PKCS7-----') + + s.write(out, p7, BIO.MemoryBuffer(self.cleartext)) + return out + + def test_store_load_info(self): + st = X509.X509_Store() + self.assertRaises(X509.X509Error, st.load_info, 'tests/ca.pem-typoname') + self.assertEqual(st.load_info('tests/ca.pem'), 1) + + def test_verify(self): + s = SMIME.SMIME() + + x509 = X509.load_cert('tests/signer.pem') + sk = X509.X509_Stack() + sk.push(x509) + s.set_x509_stack(sk) + + st = X509.X509_Store() + st.load_info('tests/ca.pem') + s.set_x509_store(st) + + p7, data = SMIME.smime_load_pkcs7_bio(self.signed) + + assert isinstance(p7, SMIME.PKCS7), p7 + v = s.verify(p7, data) + assert v == self.cleartext + + t = p7.get0_signers(sk) + assert len(t) == 1 + assert t[0].as_pem() == x509.as_pem(), t[0].as_text() + + def test_verifyBad(self): + s = SMIME.SMIME() + + x509 = X509.load_cert('tests/recipient.pem') + sk = X509.X509_Stack() + sk.push(x509) + s.set_x509_stack(sk) + + st = X509.X509_Store() + st.load_info('tests/recipient.pem') + s.set_x509_store(st) + + p7, data = SMIME.smime_load_pkcs7_bio(self.signed) + assert isinstance(p7, SMIME.PKCS7), p7 + self.assertRaises(SMIME.PKCS7_Error, s.verify, p7) # Bad signer + + def test_encrypt(self): + buf = BIO.MemoryBuffer(self.cleartext) + s = SMIME.SMIME() + + x509 = X509.load_cert('tests/recipient.pem') + sk = X509.X509_Stack() + sk.push(x509) + s.set_x509_stack(sk) + + self.assertRaises(ValueError, SMIME.Cipher, 'nosuchcipher') + + s.set_cipher(SMIME.Cipher('des_ede3_cbc')) + p7 = s.encrypt(buf) + + assert len(buf) == 0 + assert p7.type() == SMIME.PKCS7_ENVELOPED, p7.type() + assert isinstance(p7, SMIME.PKCS7), p7 + out = BIO.MemoryBuffer() + p7.write(out) + + buf = out.read() + + assert buf[:len('-----BEGIN PKCS7-----')] == '-----BEGIN PKCS7-----' + buf = buf.strip() + assert buf[-len('-----END PKCS7-----'):] == '-----END PKCS7-----' + assert len(buf) > len('-----END PKCS7-----') + len('-----BEGIN PKCS7-----') + + s.write(out, p7) + return out + + def test_decrypt(self): + s = SMIME.SMIME() + + s.load_key('tests/recipient_key.pem', 'tests/recipient.pem') + + p7, data = SMIME.smime_load_pkcs7_bio(self.encrypted) + assert isinstance(p7, SMIME.PKCS7), p7 + self.assertRaises(SMIME.SMIME_Error, s.verify, p7) # No signer + + out = s.decrypt(p7) + assert out == self.cleartext + + def test_decryptBad(self): + s = SMIME.SMIME() + + s.load_key('tests/signer_key.pem', 'tests/signer.pem') + + p7, data = SMIME.smime_load_pkcs7_bio(self.encrypted) + assert isinstance(p7, SMIME.PKCS7), p7 + self.assertRaises(SMIME.SMIME_Error, s.verify, p7) # No signer + + # Cannot decrypt: no recipient matches certificate + self.assertRaises(SMIME.PKCS7_Error, s.decrypt, p7) + + def test_signEncryptDecryptVerify(self): + # sign + buf = BIO.MemoryBuffer(self.cleartext) + s = SMIME.SMIME() + s.load_key('tests/signer_key.pem', 'tests/signer.pem') + p7 = s.sign(buf) + + # encrypt + x509 = X509.load_cert('tests/recipient.pem') + sk = X509.X509_Stack() + sk.push(x509) + s.set_x509_stack(sk) + + s.set_cipher(SMIME.Cipher('des_ede3_cbc')) + + tmp = BIO.MemoryBuffer() + s.write(tmp, p7) + + p7 = s.encrypt(tmp) + + signedEncrypted = BIO.MemoryBuffer() + s.write(signedEncrypted, p7) + + # decrypt + s = SMIME.SMIME() + + s.load_key('tests/recipient_key.pem', 'tests/recipient.pem') + + p7, data = SMIME.smime_load_pkcs7_bio(signedEncrypted) + + out = s.decrypt(p7) + + # verify + x509 = X509.load_cert('tests/signer.pem') + sk = X509.X509_Stack() + sk.push(x509) + s.set_x509_stack(sk) + + st = X509.X509_Store() + st.load_info('tests/ca.pem') + s.set_x509_store(st) + + p7_bio = BIO.MemoryBuffer(out) + p7, data = SMIME.smime_load_pkcs7_bio(p7_bio) + v = s.verify(p7) + assert v == self.cleartext + + +class WriteLoadTestCase(unittest.TestCase): + def setUp(self): + s = SMIME.SMIME() + s.load_key('tests/signer_key.pem', 'tests/signer.pem') + p7 = s.sign(BIO.MemoryBuffer('some text')) + self.filename = 'tests/sig.p7' + f = BIO.openfile(self.filename, 'wb') + assert p7.write(f) == 1 + f.close() + + p7 = s.sign(BIO.MemoryBuffer('some text'), SMIME.PKCS7_DETACHED) + self.filenameSmime = 'tests/sig.p7s' + f = BIO.openfile(self.filenameSmime, 'wb') + assert s.write(f, p7, BIO.MemoryBuffer('some text')) == 1 + f.close() + + def test_write_pkcs7_der(self): + buf = BIO.MemoryBuffer() + assert SMIME.load_pkcs7(self.filename).write_der(buf) == 1 + s = buf.read() + assert len(s) in (1204, 1243), len(s) + + def test_load_pkcs7(self): + assert SMIME.load_pkcs7(self.filename).type() == SMIME.PKCS7_SIGNED + + def test_load_pkcs7_bio(self): + f = open(self.filename, 'rb') + buf = BIO.MemoryBuffer(f.read()) + f.close() + + assert SMIME.load_pkcs7_bio(buf).type() == SMIME.PKCS7_SIGNED + + def test_load_smime(self): + a, b = SMIME.smime_load_pkcs7(self.filenameSmime) + assert isinstance(a, SMIME.PKCS7), a + assert isinstance(b, BIO.BIO), b + assert a.type() == SMIME.PKCS7_SIGNED + + def test_load_smime_bio(self): + f = open(self.filenameSmime, 'rb') + buf = BIO.MemoryBuffer(f.read()) + f.close() + + a, b = SMIME.smime_load_pkcs7_bio(buf) + assert isinstance(a, SMIME.PKCS7), a + assert isinstance(b, BIO.BIO), b + assert a.type() == SMIME.PKCS7_SIGNED + + +def suite(): + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(SMIMETestCase)) + suite.addTest(unittest.makeSuite(WriteLoadTestCase)) + return suite + + +if __name__ == '__main__': + Rand.load_file('randpool.dat', -1) + unittest.TextTestRunner().run(suite()) + Rand.save_file('randpool.dat') + diff --git a/tests/test_ssl.py b/tests/test_ssl.py new file mode 100644 index 0000000..701484d --- /dev/null +++ b/tests/test_ssl.py @@ -0,0 +1,1108 @@ +#!/usr/bin/env python + +"""Unit tests for M2Crypto.SSL. + +Copyright (c) 2000-2004 Ng Pheng Siong. All rights reserved. + +Copyright (c) 2009-2010 Heikki Toivonen. All rights reserved. +""" + +""" +TODO + +Server tests: +- ??? + +Others: +- ssl_dispatcher +- SSLServer +- ForkingSSLServer +- ThreadingSSLServer +""" + +import os, socket, string, sys, tempfile, thread, time, unittest +from M2Crypto import Rand, SSL, m2, Err + +from fips import fips_mode + +srv_host = 'localhost' +srv_port = 64000 + +def verify_cb_new_function(ok, store): + try: + assert not ok + err = store.get_error() + assert err in [m2.X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT, + m2.X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY, + m2.X509_V_ERR_CERT_UNTRUSTED, + m2.X509_V_ERR_UNABLE_TO_VERIFY_LEAF_SIGNATURE] + assert store.get_error_depth() == 0 + app_data = m2.x509_store_ctx_get_app_data(store.ctx) + assert app_data + x509 = store.get_current_cert() + assert x509 + stack = store.get1_chain() + assert len(stack) == 1 + assert stack[0].as_pem() == x509.as_pem() + except AssertionError, e: + # If we let exceptions propagate from here the + # caller may see strange errors. This is cleaner. + return 0 + return 1 + +class VerifyCB: + def __call__(self, ok, store): + return verify_cb_new_function(ok, store) + +sleepTime = float(os.getenv('M2CRYPTO_TEST_SSL_SLEEP', 0.5)) + +def find_openssl(): + if os.name == 'nt' or sys.platform == 'cygwin': + openssl = 'openssl.exe' + else: + openssl = 'openssl' + + plist = os.environ['PATH'].split(os.pathsep) + for p in plist: + try: + dir = os.listdir(p) + if openssl in dir: + return True + except: + pass + return False + +class BaseSSLClientTestCase(unittest.TestCase): + + openssl_in_path = find_openssl() + + def start_server(self, args): + if not self.openssl_in_path: + raise Exception('openssl command not in PATH') + + pid = os.fork() + if pid == 0: + # openssl must be started in the tests directory for it + # to find the .pem files + os.chdir('tests') + try: + os.execvp('openssl', args) + finally: + os.chdir('..') + + else: + time.sleep(sleepTime) + return pid + + def stop_server(self, pid): + os.kill(pid, 1) + os.waitpid(pid, 0) + + def http_get(self, s): + s.send('GET / HTTP/1.0\n\n') + resp = '' + while 1: + try: + r = s.recv(4096) + if not r: + break + except SSL.SSLError: # s_server throws an 'unexpected eof'... + break + resp = resp + r + return resp + + def setUp(self): + self.srv_host = srv_host + self.srv_port = srv_port + self.srv_addr = (srv_host, srv_port) + self.srv_url = 'https://%s:%s/' % (srv_host, srv_port) + self.args = ['s_server', '-quiet', '-www', + #'-cert', 'server.pem', Implicitly using this + '-accept', str(self.srv_port)] + + def tearDown(self): + global srv_port + srv_port = srv_port - 1 + + +class PassSSLClientTestCase(BaseSSLClientTestCase): + + def test_pass(self): + pass + +class HttpslibSSLClientTestCase(BaseSSLClientTestCase): + + def test_HTTPSConnection(self): + pid = self.start_server(self.args) + try: + from M2Crypto import httpslib + c = httpslib.HTTPSConnection(srv_host, srv_port) + c.request('GET', '/') + data = c.getresponse().read() + c.close() + finally: + self.stop_server(pid) + self.failIf(string.find(data, 's_server -quiet -www') == -1) + + def test_HTTPSConnection_resume_session(self): + pid = self.start_server(self.args) + try: + from M2Crypto import httpslib + ctx = SSL.Context() + ctx.load_verify_locations(cafile='tests/ca.pem') + ctx.load_cert('tests/x509.pem') + ctx.set_verify(SSL.verify_peer | SSL.verify_fail_if_no_peer_cert, 1) + ctx.set_session_cache_mode(m2.SSL_SESS_CACHE_CLIENT) + c = httpslib.HTTPSConnection(srv_host, srv_port, ssl_context=ctx) + c.request('GET', '/') + ses = c.get_session() + t = ses.as_text() + data = c.getresponse().read() + # Appearently closing connection here screws session; Ali Polatel? + # c.close() + + ctx2 = SSL.Context() + ctx2.load_verify_locations(cafile='tests/ca.pem') + ctx2.load_cert('tests/x509.pem') + ctx2.set_verify(SSL.verify_peer | SSL.verify_fail_if_no_peer_cert, 1) + ctx2.set_session_cache_mode(m2.SSL_SESS_CACHE_CLIENT) + c2 = httpslib.HTTPSConnection(srv_host, srv_port, ssl_context=ctx2) + c2.set_session(ses) + c2.request('GET', '/') + ses2 = c2.get_session() + t2 = ses2.as_text() + data = c2.getresponse().read() + c.close() + c2.close() + assert t == t2, "Sessions did not match" + finally: + self.stop_server(pid) + self.failIf(string.find(data, 's_server -quiet -www') == -1) + + def test_HTTPSConnection_secure_context(self): + pid = self.start_server(self.args) + try: + from M2Crypto import httpslib + ctx = SSL.Context() + ctx.set_verify(SSL.verify_peer | SSL.verify_fail_if_no_peer_cert, 9) + ctx.load_verify_locations('tests/ca.pem') + c = httpslib.HTTPSConnection(srv_host, srv_port, ssl_context=ctx) + c.request('GET', '/') + data = c.getresponse().read() + c.close() + finally: + self.stop_server(pid) + self.failIf(string.find(data, 's_server -quiet -www') == -1) + + def test_HTTPSConnection_secure_context_fail(self): + pid = self.start_server(self.args) + try: + from M2Crypto import httpslib + ctx = SSL.Context() + ctx.set_verify(SSL.verify_peer | SSL.verify_fail_if_no_peer_cert, 9) + ctx.load_verify_locations('tests/server.pem') + c = httpslib.HTTPSConnection(srv_host, srv_port, ssl_context=ctx) + self.assertRaises(SSL.SSLError, c.request, 'GET', '/') + c.close() + finally: + self.stop_server(pid) + + def test_HTTPS(self): + pid = self.start_server(self.args) + try: + from M2Crypto import httpslib + c = httpslib.HTTPS(srv_host, srv_port) + c.putrequest('GET', '/') + c.putheader('Accept', 'text/html') + c.putheader('Accept', 'text/plain') + c.endheaders() + err, msg, headers = c.getreply() + assert err == 200, err + f = c.getfile() + data = f.read() + c.close() + finally: + self.stop_server(pid) + self.failIf(string.find(data, 's_server -quiet -www') == -1) + + def test_HTTPS_secure_context(self): + pid = self.start_server(self.args) + try: + from M2Crypto import httpslib + ctx = SSL.Context() + ctx.set_verify(SSL.verify_peer | SSL.verify_fail_if_no_peer_cert, 9) + ctx.load_verify_locations('tests/ca.pem') + c = httpslib.HTTPS(srv_host, srv_port, ssl_context=ctx) + c.putrequest('GET', '/') + c.putheader('Accept', 'text/html') + c.putheader('Accept', 'text/plain') + c.endheaders() + err, msg, headers = c.getreply() + assert err == 200, err + f = c.getfile() + data = f.read() + c.close() + finally: + self.stop_server(pid) + self.failIf(string.find(data, 's_server -quiet -www') == -1) + + def test_HTTPS_secure_context_fail(self): + pid = self.start_server(self.args) + try: + from M2Crypto import httpslib + ctx = SSL.Context() + ctx.set_verify(SSL.verify_peer | SSL.verify_fail_if_no_peer_cert, 9) + ctx.load_verify_locations('tests/server.pem') + c = httpslib.HTTPS(srv_host, srv_port, ssl_context=ctx) + c.putrequest('GET', '/') + c.putheader('Accept', 'text/html') + c.putheader('Accept', 'text/plain') + self.assertRaises(SSL.SSLError, c.endheaders) + c.close() + finally: + self.stop_server(pid) + + def test_HTTPSConnection_illegalkeywordarg(self): + from M2Crypto import httpslib + self.assertRaises(ValueError, httpslib.HTTPSConnection, 'example.org', + badKeyword=True) + + +class MiscSSLClientTestCase(BaseSSLClientTestCase): + + def test_no_connection(self): + ctx = SSL.Context() + s = SSL.Connection(ctx) + + def test_server_simple(self): + pid = self.start_server(self.args) + try: + self.assertRaises(ValueError, SSL.Context, 'tlsv5') + ctx = SSL.Context() + s = SSL.Connection(ctx) + s.connect(self.srv_addr) + self.assertRaises(ValueError, s.read, 0) + data = self.http_get(s) + s.close() + finally: + self.stop_server(pid) + self.failIf(string.find(data, 's_server -quiet -www') == -1) + + def test_server_simple_secure_context(self): + pid = self.start_server(self.args) + try: + ctx = SSL.Context() + ctx.set_verify(SSL.verify_peer | SSL.verify_fail_if_no_peer_cert, 9) + ctx.load_verify_locations('tests/ca.pem') + s = SSL.Connection(ctx) + s.connect(self.srv_addr) + data = self.http_get(s) + s.close() + finally: + self.stop_server(pid) + self.failIf(string.find(data, 's_server -quiet -www') == -1) + + def test_server_simple_secure_context_fail(self): + pid = self.start_server(self.args) + try: + ctx = SSL.Context() + ctx.set_verify(SSL.verify_peer | SSL.verify_fail_if_no_peer_cert, 9) + ctx.load_verify_locations('tests/server.pem') + s = SSL.Connection(ctx) + self.assertRaises(SSL.SSLError, s.connect, self.srv_addr) + s.close() + finally: + self.stop_server(pid) + + def test_server_simple_timeouts(self): + pid = self.start_server(self.args) + try: + self.assertRaises(ValueError, SSL.Context, 'tlsv5') + ctx = SSL.Context() + s = SSL.Connection(ctx) + + r = s.get_socket_read_timeout() + w = s.get_socket_write_timeout() + assert r.sec == 0, r.sec + assert r.microsec == 0, r.microsec + assert w.sec == 0, w.sec + assert w.microsec == 0, w.microsec + + s.set_socket_read_timeout(SSL.timeout()) + s.set_socket_write_timeout(SSL.timeout(909,9)) + r = s.get_socket_read_timeout() + w = s.get_socket_write_timeout() + assert r.sec == 600, r.sec + assert r.microsec == 0, r.microsec + assert w.sec == 909, w.sec + #assert w.microsec == 9, w.microsec XXX 4000 + + s.connect(self.srv_addr) + data = self.http_get(s) + s.close() + finally: + self.stop_server(pid) + self.failIf(string.find(data, 's_server -quiet -www') == -1) + + def test_tls1_nok(self): + if fips_mode: # TLS is required in FIPS mode + return + self.args.append('-no_tls1') + pid = self.start_server(self.args) + try: + ctx = SSL.Context('tlsv1') + s = SSL.Connection(ctx) + try: + s.connect(self.srv_addr) + except SSL.SSLError, e: + self.failUnlessEqual(e[0], 'wrong version number') + s.close() + finally: + self.stop_server(pid) + + def test_tls1_ok(self): + self.args.append('-tls1') + pid = self.start_server(self.args) + try: + ctx = SSL.Context('tlsv1') + s = SSL.Connection(ctx) + s.connect(self.srv_addr) + data = self.http_get(s) + s.close() + finally: + self.stop_server(pid) + self.failIf(string.find(data, 's_server -quiet -www') == -1) + + def test_sslv23_no_v2(self): + if fips_mode: # TLS is required in FIPS mode + return + self.args.append('-no_tls1') + pid = self.start_server(self.args) + try: + ctx = SSL.Context('sslv23') + s = SSL.Connection(ctx) + s.connect(self.srv_addr) + self.failUnlessEqual(s.get_version(), 'SSLv3') + s.close() + finally: + self.stop_server(pid) + + def test_sslv23_no_v2_no_service(self): + if fips_mode: # TLS is required in FIPS mode + return + self.args = self.args + ['-no_tls1', '-no_ssl3'] + pid = self.start_server(self.args) + try: + ctx = SSL.Context('sslv23') + s = SSL.Connection(ctx) + self.assertRaises(SSL.SSLError, s.connect, self.srv_addr) + s.close() + finally: + self.stop_server(pid) + + def test_sslv23_weak_crypto(self): + if fips_mode: # TLS is required in FIPS mode + return + self.args = self.args + ['-no_tls1', '-no_ssl3'] + pid = self.start_server(self.args) + try: + ctx = SSL.Context('sslv23', weak_crypto=1) + s = SSL.Connection(ctx) + if m2.OPENSSL_VERSION_NUMBER < 0x10000000: # SSLv2 ciphers disabled by default in newer OpenSSL + s.connect(self.srv_addr) + self.failUnlessEqual(s.get_version(), 'SSLv2') + else: + self.assertRaises(SSL.SSLError, s.connect, self.srv_addr) + s.close() + finally: + self.stop_server(pid) + + def test_cipher_mismatch(self): + self.args = self.args + ['-cipher', 'AES256-SHA'] + pid = self.start_server(self.args) + try: + ctx = SSL.Context() + s = SSL.Connection(ctx) + s.set_cipher_list('AES128-SHA') + try: + s.connect(self.srv_addr) + except SSL.SSLError, e: + self.failUnlessEqual(e[0], 'sslv3 alert handshake failure') + s.close() + finally: + self.stop_server(pid) + + def test_no_such_cipher(self): + self.args = self.args + ['-cipher', 'AES128-SHA'] + pid = self.start_server(self.args) + try: + ctx = SSL.Context() + s = SSL.Connection(ctx) + s.set_cipher_list('EXP-RC2-MD5') + try: + s.connect(self.srv_addr) + except SSL.SSLError, e: + self.failUnlessEqual(e[0], 'no ciphers available') + s.close() + finally: + self.stop_server(pid) + + def test_no_weak_cipher(self): + if fips_mode: # Weak ciphers are prohibited + return + self.args = self.args + ['-cipher', 'EXP'] + pid = self.start_server(self.args) + try: + ctx = SSL.Context() + s = SSL.Connection(ctx) + try: + s.connect(self.srv_addr) + except SSL.SSLError, e: + self.failUnlessEqual(e[0], 'sslv3 alert handshake failure') + s.close() + finally: + self.stop_server(pid) + + def test_use_weak_cipher(self): + if fips_mode: # Weak ciphers are prohibited + return + self.args = self.args + ['-cipher', 'EXP'] + pid = self.start_server(self.args) + try: + ctx = SSL.Context(weak_crypto=1) + s = SSL.Connection(ctx) + s.connect(self.srv_addr) + data = self.http_get(s) + s.close() + finally: + self.stop_server(pid) + self.failIf(string.find(data, 's_server -quiet -www') == -1) + + def test_cipher_ok(self): + self.args = self.args + ['-cipher', 'AES128-SHA'] + pid = self.start_server(self.args) + try: + ctx = SSL.Context() + s = SSL.Connection(ctx) + s.set_cipher_list('AES128-SHA') + s.connect(self.srv_addr) + data = self.http_get(s) + + assert s.get_cipher().name() == 'AES128-SHA', s.get_cipher().name() + + cipher_stack = s.get_ciphers() + assert cipher_stack[0].name() == 'AES128-SHA', cipher_stack[0].name() + self.assertRaises(IndexError, cipher_stack.__getitem__, 2) + # For some reason there are 2 entries in the stack + #assert len(cipher_stack) == 1, len(cipher_stack) + assert s.get_cipher_list() == 'AES128-SHA', s.get_cipher_list() + + # Test Cipher_Stack iterator + i = 0 + for cipher in cipher_stack: + i += 1 + assert cipher.name() == 'AES128-SHA', '"%s"' % cipher.name() + self.assertEqual('AES128-SHA-128', str(cipher)) + # For some reason there are 2 entries in the stack + #assert i == 1, i + self.assertEqual(i, len(cipher_stack)) + + s.close() + finally: + self.stop_server(pid) + self.failIf(string.find(data, 's_server -quiet -www') == -1) + + def verify_cb_new(self, ok, store): + return verify_cb_new_function(ok, store) + + def test_verify_cb_new(self): + pid = self.start_server(self.args) + try: + ctx = SSL.Context() + ctx.set_verify(SSL.verify_peer | SSL.verify_fail_if_no_peer_cert, 9, + self.verify_cb_new) + s = SSL.Connection(ctx) + try: + s.connect(self.srv_addr) + except SSL.SSLError, e: + assert 0, e + data = self.http_get(s) + s.close() + finally: + self.stop_server(pid) + self.failIf(string.find(data, 's_server -quiet -www') == -1) + + def test_verify_cb_new_class(self): + pid = self.start_server(self.args) + try: + ctx = SSL.Context() + ctx.set_verify(SSL.verify_peer | SSL.verify_fail_if_no_peer_cert, 9, + VerifyCB()) + s = SSL.Connection(ctx) + try: + s.connect(self.srv_addr) + except SSL.SSLError, e: + assert 0, e + data = self.http_get(s) + s.close() + finally: + self.stop_server(pid) + self.failIf(string.find(data, 's_server -quiet -www') == -1) + + def test_verify_cb_new_function(self): + pid = self.start_server(self.args) + try: + ctx = SSL.Context() + ctx.set_verify(SSL.verify_peer | SSL.verify_fail_if_no_peer_cert, 9, + verify_cb_new_function) + s = SSL.Connection(ctx) + try: + s.connect(self.srv_addr) + except SSL.SSLError, e: + assert 0, e + data = self.http_get(s) + s.close() + finally: + self.stop_server(pid) + self.failIf(string.find(data, 's_server -quiet -www') == -1) + + def test_verify_cb_lambda(self): + pid = self.start_server(self.args) + try: + ctx = SSL.Context() + ctx.set_verify(SSL.verify_peer | SSL.verify_fail_if_no_peer_cert, 9, + lambda ok, store: 1) + s = SSL.Connection(ctx) + try: + s.connect(self.srv_addr) + except SSL.SSLError, e: + assert 0, e + data = self.http_get(s) + s.close() + finally: + self.stop_server(pid) + self.failIf(string.find(data, 's_server -quiet -www') == -1) + + def verify_cb_exception(self, ok, store): + raise Exception, 'We should fail verification' + + def test_verify_cb_exception(self): + pid = self.start_server(self.args) + try: + ctx = SSL.Context() + ctx.set_verify(SSL.verify_peer | SSL.verify_fail_if_no_peer_cert, 9, + self.verify_cb_exception) + s = SSL.Connection(ctx) + self.assertRaises(SSL.SSLError, s.connect, self.srv_addr) + s.close() + finally: + self.stop_server(pid) + + def test_verify_cb_not_callable(self): + ctx = SSL.Context() + self.assertRaises(TypeError, + ctx.set_verify, + SSL.verify_peer | SSL.verify_fail_if_no_peer_cert, + 9, + 1) + + def test_verify_cb_wrong_callable(self): + pid = self.start_server(self.args) + try: + ctx = SSL.Context() + ctx.set_verify(SSL.verify_peer | SSL.verify_fail_if_no_peer_cert, 9, + lambda _: '') + s = SSL.Connection(ctx) + self.assertRaises(SSL.SSLError, s.connect, self.srv_addr) + s.close() + finally: + self.stop_server(pid) + + def verify_cb_old(self, ctx_ptr, x509_ptr, err, depth, ok): + try: + from M2Crypto import X509 + assert not ok + assert err == m2.X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT or \ + err == m2.X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY or \ + err == m2.X509_V_ERR_CERT_UNTRUSTED or \ + err == m2.X509_V_ERR_UNABLE_TO_VERIFY_LEAF_SIGNATURE + assert m2.ssl_ctx_get_cert_store(ctx_ptr) + assert X509.X509(x509_ptr).as_pem() + except AssertionError: + # If we let exceptions propagate from here the + # caller may see strange errors. This is cleaner. + return 0 + return 1 + + def test_verify_cb_old(self): + pid = self.start_server(self.args) + try: + ctx = SSL.Context() + ctx.set_verify(SSL.verify_peer | SSL.verify_fail_if_no_peer_cert, 9, + self.verify_cb_old) + s = SSL.Connection(ctx) + try: + s.connect(self.srv_addr) + except SSL.SSLError, e: + assert 0, e + data = self.http_get(s) + s.close() + finally: + self.stop_server(pid) + self.failIf(string.find(data, 's_server -quiet -www') == -1) + + def test_verify_allow_unknown_old(self): + pid = self.start_server(self.args) + try: + ctx = SSL.Context() + ctx.set_verify(SSL.verify_peer | SSL.verify_fail_if_no_peer_cert, 9, + SSL.cb.ssl_verify_callback) + ctx.set_allow_unknown_ca(1) + s = SSL.Connection(ctx) + try: + s.connect(self.srv_addr) + except SSL.SSLError, e: + assert 0, e + data = self.http_get(s) + s.close() + finally: + self.stop_server(pid) + self.failIf(string.find(data, 's_server -quiet -www') == -1) + + def test_verify_allow_unknown_new(self): + pid = self.start_server(self.args) + try: + ctx = SSL.Context() + ctx.set_verify(SSL.verify_peer | SSL.verify_fail_if_no_peer_cert, 9, + SSL.cb.ssl_verify_callback_allow_unknown_ca) + s = SSL.Connection(ctx) + try: + s.connect(self.srv_addr) + except SSL.SSLError, e: + assert 0, e + data = self.http_get(s) + s.close() + finally: + self.stop_server(pid) + self.failIf(string.find(data, 's_server -quiet -www') == -1) + + def test_verify_cert(self): + pid = self.start_server(self.args) + try: + ctx = SSL.Context() + ctx.set_verify(SSL.verify_peer | SSL.verify_fail_if_no_peer_cert, 9) + ctx.load_verify_locations('tests/ca.pem') + s = SSL.Connection(ctx) + try: + s.connect(self.srv_addr) + except SSL.SSLError, e: + assert 0, e + data = self.http_get(s) + s.close() + finally: + self.stop_server(pid) + self.failIf(string.find(data, 's_server -quiet -www') == -1) + + def test_verify_cert_fail(self): + pid = self.start_server(self.args) + try: + ctx = SSL.Context() + ctx.set_verify(SSL.verify_peer | SSL.verify_fail_if_no_peer_cert, 9) + ctx.load_verify_locations('tests/server.pem') + s = SSL.Connection(ctx) + self.assertRaises(SSL.SSLError, s.connect, self.srv_addr) + s.close() + finally: + self.stop_server(pid) + + def test_verify_cert_mutual_auth(self): + self.args.extend(['-Verify', '2', '-CAfile', 'ca.pem']) + pid = self.start_server(self.args) + try: + ctx = SSL.Context() + ctx.set_verify(SSL.verify_peer | SSL.verify_fail_if_no_peer_cert, 9) + ctx.load_verify_locations('tests/ca.pem') + ctx.load_cert('tests/x509.pem') + s = SSL.Connection(ctx) + try: + s.connect(self.srv_addr) + except SSL.SSLError, e: + assert 0, e + data = self.http_get(s) + s.close() + finally: + self.stop_server(pid) + self.failIf(string.find(data, 's_server -quiet -www') == -1) + + def test_verify_cert_mutual_auth_servernbio(self): + self.args.extend(['-Verify', '2', '-CAfile', 'ca.pem', '-nbio']) + pid = self.start_server(self.args) + try: + ctx = SSL.Context() + ctx.set_verify(SSL.verify_peer | SSL.verify_fail_if_no_peer_cert, 9) + ctx.load_verify_locations('tests/ca.pem') + ctx.load_cert('tests/x509.pem') + s = SSL.Connection(ctx) + try: + s.connect(self.srv_addr) + except SSL.SSLError, e: + assert 0, e + data = self.http_get(s) + s.close() + finally: + self.stop_server(pid) + self.failIf(string.find(data, 's_server -quiet -www') == -1) + + def test_verify_cert_mutual_auth_fail(self): + self.args.extend(['-Verify', '2', '-CAfile', 'ca.pem']) + pid = self.start_server(self.args) + try: + ctx = SSL.Context() + ctx.set_verify(SSL.verify_peer | SSL.verify_fail_if_no_peer_cert, 9) + ctx.load_verify_locations('tests/ca.pem') + s = SSL.Connection(ctx) + self.assertRaises(SSL.SSLError, s.connect, self.srv_addr) + s.close() + finally: + self.stop_server(pid) + + def test_verify_nocert_fail(self): + self.args.extend(['-nocert']) + pid = self.start_server(self.args) + try: + ctx = SSL.Context() + ctx.set_verify(SSL.verify_peer | SSL.verify_fail_if_no_peer_cert, 9) + ctx.load_verify_locations('tests/ca.pem') + s = SSL.Connection(ctx) + self.assertRaises(SSL.SSLError, s.connect, self.srv_addr) + s.close() + finally: + self.stop_server(pid) + + def test_blocking0(self): + pid = self.start_server(self.args) + try: + ctx = SSL.Context() + s = SSL.Connection(ctx) + s.setblocking(0) + self.assertRaises(Exception, s.connect, self.srv_addr) + s.close() + finally: + self.stop_server(pid) + + def test_blocking1(self): + pid = self.start_server(self.args) + try: + ctx = SSL.Context() + s = SSL.Connection(ctx) + s.setblocking(1) + try: + s.connect(self.srv_addr) + except SSL.SSLError, e: + assert 0, e + data = self.http_get(s) + s.close() + finally: + self.stop_server(pid) + self.failIf(string.find(data, 's_server -quiet -www') == -1) + + def test_makefile(self): + pid = self.start_server(self.args) + try: + ctx = SSL.Context() + s = SSL.Connection(ctx) + try: + s.connect(self.srv_addr) + except SSL.SSLError, e: + assert 0, e + bio = s.makefile('rw') + #s.close() # XXX bug 6628? + bio.write('GET / HTTP/1.0\n\n') + bio.flush() + data = bio.read() + bio.close() + s.close() + finally: + self.stop_server(pid) + self.failIf(string.find(data, 's_server -quiet -www') == -1) + + def test_makefile_err(self): + pid = self.start_server(self.args) + try: + ctx = SSL.Context() + s = SSL.Connection(ctx) + try: + s.connect(self.srv_addr) + except SSL.SSLError, e: + assert 0, e + f = s.makefile() + data = self.http_get(s) + s.close() + del f + del s + err_code = Err.peek_error_code() + assert not err_code, 'Unexpected error: %s' % err_code + err = Err.get_error() + assert not err, 'Unexpected error: %s' % err + finally: + self.stop_server(pid) + self.failIf(string.find(data, 's_server -quiet -www') == -1) + + def test_info_callback(self): + pid = self.start_server(self.args) + try: + ctx = SSL.Context() + ctx.set_info_callback() + s = SSL.Connection(ctx) + s.connect(self.srv_addr) + data = self.http_get(s) + s.close() + finally: + self.stop_server(pid) + self.failIf(string.find(data, 's_server -quiet -www') == -1) + + +class UrllibSSLClientTestCase(BaseSSLClientTestCase): + + def test_urllib(self): + pid = self.start_server(self.args) + try: + from M2Crypto import m2urllib + url = m2urllib.FancyURLopener() + url.addheader('Connection', 'close') + u = url.open('https://%s:%s/' % (srv_host, srv_port)) + data = u.read() + u.close() + finally: + self.stop_server(pid) + self.failIf(string.find(data, 's_server -quiet -www') == -1) + + # XXX Don't actually know how to use m2urllib safely! + #def test_urllib_secure_context(self): + #def test_urllib_secure_context_fail(self): + + # XXX Don't actually know how to use m2urllib safely! + #def test_urllib_safe_context(self): + #def test_urllib_safe_context_fail(self): + + +class Urllib2SSLClientTestCase(BaseSSLClientTestCase): + + if sys.version_info >= (2,4): + def test_urllib2(self): + pid = self.start_server(self.args) + try: + from M2Crypto import m2urllib2 + opener = m2urllib2.build_opener() + opener.addheaders = [('Connection', 'close')] + u = opener.open('https://%s:%s/' % (srv_host, srv_port)) + data = u.read() + u.close() + finally: + self.stop_server(pid) + self.failIf(string.find(data, 's_server -quiet -www') == -1) + + def test_urllib2_secure_context(self): + pid = self.start_server(self.args) + try: + ctx = SSL.Context() + ctx.set_verify(SSL.verify_peer | SSL.verify_fail_if_no_peer_cert, 9) + ctx.load_verify_locations('tests/ca.pem') + + from M2Crypto import m2urllib2 + opener = m2urllib2.build_opener(ctx) + opener.addheaders = [('Connection', 'close')] + u = opener.open('https://%s:%s/' % (srv_host, srv_port)) + data = u.read() + u.close() + finally: + self.stop_server(pid) + self.failIf(string.find(data, 's_server -quiet -www') == -1) + + def test_urllib2_secure_context_fail(self): + pid = self.start_server(self.args) + try: + ctx = SSL.Context() + ctx.set_verify(SSL.verify_peer | SSL.verify_fail_if_no_peer_cert, 9) + ctx.load_verify_locations('tests/server.pem') + + from M2Crypto import m2urllib2 + opener = m2urllib2.build_opener(ctx) + opener.addheaders = [('Connection', 'close')] + self.assertRaises(SSL.SSLError, opener.open, 'https://%s:%s/' % (srv_host, srv_port)) + finally: + self.stop_server(pid) + + def test_z_urllib2_opener(self): + pid = self.start_server(self.args) + try: + ctx = SSL.Context() + + from M2Crypto import m2urllib2 + opener = m2urllib2.build_opener(ctx, m2urllib2.HTTPBasicAuthHandler()) + m2urllib2.install_opener(opener) + req = m2urllib2.Request('https://%s:%s/' % (srv_host, srv_port)) + u = m2urllib2.urlopen(req) + data = u.read() + u.close() + finally: + self.stop_server(pid) + self.failIf(string.find(data, 's_server -quiet -www') == -1) + + def test_urllib2_opener_handlers(self): + ctx = SSL.Context() + + from M2Crypto import m2urllib2 + opener = m2urllib2.build_opener(ctx, + m2urllib2.HTTPBasicAuthHandler()) + + def test_urllib2_leak(self): + pid = self.start_server(self.args) + try: + import gc + from M2Crypto import m2urllib2 + o = m2urllib2.build_opener() + r = o.open('https://%s:%s/' % (srv_host, srv_port)) + s = [r.fp._sock.fp] + r.close() + self.assertEqual(len(gc.get_referrers(s[0])), 1) + finally: + self.stop_server(pid) + + +class TwistedSSLClientTestCase(BaseSSLClientTestCase): + + def test_twisted_wrapper(self): + # Test only when twisted and ZopeInterfaces are present + try: + from twisted.internet.protocol import ClientFactory + from twisted.protocols.basic import LineReceiver + from twisted.internet import reactor + import M2Crypto.SSL.TwistedProtocolWrapper as wrapper + except ImportError: + import warnings + warnings.warn('Skipping twisted wrapper test because twisted not found') + return + + class EchoClient(LineReceiver): + def connectionMade(self): + self.sendLine('GET / HTTP/1.0\n\n') + + def lineReceived(self, line): + global twisted_data + twisted_data += line + + class EchoClientFactory(ClientFactory): + protocol = EchoClient + + def clientConnectionFailed(self, connector, reason): + reactor.stop() + assert 0, reason + + def clientConnectionLost(self, connector, reason): + reactor.stop() + + pid = self.start_server(self.args) + + class ContextFactory: + def getContext(self): + return SSL.Context() + + try: + global twisted_data + twisted_data = '' + + contextFactory = ContextFactory() + factory = EchoClientFactory() + wrapper.connectSSL(srv_host, srv_port, factory, contextFactory) + reactor.run() # This will block until reactor.stop() is called + finally: + self.stop_server(pid) + self.failIf(string.find(twisted_data, 's_server -quiet -www') == -1) + + +twisted_data = '' + + +class XmlRpcLibTestCase(unittest.TestCase): + def test_lib(self): + from M2Crypto import m2xmlrpclib + m2xmlrpclib.SSL_Transport() + # XXX need server to test against + + +class FtpsLibTestCase(unittest.TestCase): + def test_lib(self): + from M2Crypto import ftpslib + ftpslib.FTP_TLS() + # XXX need server to test against + + +class SessionTestCase(unittest.TestCase): + def test_session_load_bad(self): + self.assertRaises(SSL.SSLError, SSL.Session.load_session, + 'tests/signer.pem') + +class FtpslibTestCase(unittest.TestCase): + def test_26_compat(self): + from M2Crypto import ftpslib + f = ftpslib.FTP_TLS() + # 2.6 used to raise AttributeError: + self.assertRaises(socket.gaierror, f.connect, 'no-such-host-dfgHJK56789', 990) + + +def suite(): + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(SessionTestCase)) + suite.addTest(unittest.makeSuite(XmlRpcLibTestCase)) + suite.addTest(unittest.makeSuite(FtpsLibTestCase)) + suite.addTest(unittest.makeSuite(PassSSLClientTestCase)) + suite.addTest(unittest.makeSuite(HttpslibSSLClientTestCase)) + suite.addTest(unittest.makeSuite(UrllibSSLClientTestCase)) + suite.addTest(unittest.makeSuite(Urllib2SSLClientTestCase)) + suite.addTest(unittest.makeSuite(MiscSSLClientTestCase)) + suite.addTest(unittest.makeSuite(FtpslibTestCase)) + try: + import M2Crypto.SSL.TwistedProtocolWrapper as wrapper + suite.addTest(unittest.makeSuite(TwistedSSLClientTestCase)) + except ImportError: + pass + return suite + + +def zap_servers(): + s = 's_server' + fn = tempfile.mktemp() + cmd = 'ps | egrep %s > %s' % (s, fn) + os.system(cmd) + f = open(fn) + while 1: + ps = f.readline() + if not ps: + break + chunk = string.split(ps) + pid, cmd = chunk[0], chunk[4] + if cmd == s: + os.kill(int(pid), 1) + f.close() + os.unlink(fn) + + +if __name__ == '__main__': + report_leaks = 0 + + if report_leaks: + import gc + gc.enable() + gc.set_debug(gc.DEBUG_LEAK & ~gc.DEBUG_SAVEALL) + + try: + Rand.load_file('randpool.dat', -1) + unittest.TextTestRunner().run(suite()) + Rand.save_file('randpool.dat') + finally: + zap_servers() + + if report_leaks: + import alltests + alltests.dump_garbage() diff --git a/tests/test_ssl_offline.py b/tests/test_ssl_offline.py new file mode 100644 index 0000000..ba77434 --- /dev/null +++ b/tests/test_ssl_offline.py @@ -0,0 +1,60 @@ +"""Unit tests for M2Crypto.SSL offline parts + +Copyright (C) 2006 Open Source Applications Foundation. All Rights Reserved. + +Copyright (C) 2009-2010 Heikki Toivonen. All Rights Reserved. +""" + +import unittest, doctest +from M2Crypto.SSL import Checker +from M2Crypto import X509 +from M2Crypto import SSL +from test_ssl import srv_host + + +class CheckerTestCase(unittest.TestCase): + def test_checker(self): + + check = Checker.Checker(host=srv_host, + peerCertHash='7B754EFA41A264AAD370D43460BC8229F9354ECE') + x509 = X509.load_cert('tests/server.pem') + assert check(x509, srv_host) + self.assertRaises(Checker.WrongHost, check, x509, 'example.com') + + doctest.testmod(Checker) + + +class ContextTestCase(unittest.TestCase): + def test_ctx_load_verify_locations(self): + ctx = SSL.Context() + self.assertRaises(ValueError, ctx.load_verify_locations, None, None) + + def test_map(self): + from M2Crypto.SSL.Context import map, _ctxmap + assert isinstance(map(), _ctxmap) + ctx = SSL.Context() + assert map() + ctx.close() + assert map() is _ctxmap.singleton + + def test_certstore(self): + ctx = SSL.Context() + ctx.set_verify(SSL.verify_peer | SSL.verify_fail_if_no_peer_cert, 9) + ctx.load_verify_locations('tests/ca.pem') + ctx.load_cert('tests/x509.pem') + + store = ctx.get_cert_store() + assert isinstance(store, X509.X509_Store) + + +def suite(): + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(CheckerTestCase)) + suite.addTest(unittest.makeSuite(ContextTestCase)) + return suite + + +if __name__ == '__main__': + Rand.load_file('randpool.dat', -1) + unittest.TextTestRunner().run(suite()) + Rand.save_file('randpool.dat') diff --git a/tests/test_ssl_win.py b/tests/test_ssl_win.py new file mode 100644 index 0000000..0d13bd0 --- /dev/null +++ b/tests/test_ssl_win.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python + +"""Unit tests for M2Crypto.SSL. + +Win32 version - requires Mark Hammond's Win32 extensions and openssl.exe +on your PATH. + +Copyright (c) 2000-2001 Ng Pheng Siong. All rights reserved.""" + +import os, os.path, string, time, unittest +try: + import win32process +except ImportError: + win32process = None + +if win32process: + from M2Crypto import Rand, SSL + import test_ssl + + def find_openssl(): + plist = os.environ['PATH'].split(';') + for p in plist: + try: + dir = os.listdir(p) + if 'openssl.exe' in dir: + return os.path.join(p, 'openssl.exe') + except WindowsError: + pass + return None + + + srv_host = 'localhost' + srv_port = 64000 + + class SSLWinClientTestCase(test_ssl.SSLClientTestCase): + + startupinfo = win32process.STARTUPINFO() + openssl = find_openssl() + + def start_server(self, args): + # openssl must be started in the tests directory for it + # to find the .pem files + os.chdir('tests') + try: + hproc, hthread, pid, tid = win32process.CreateProcess(self.openssl, + string.join(args), None, None, 0, win32process.DETACHED_PROCESS, + None, None, self.startupinfo) + finally: + os.chdir('..') + time.sleep(0.3) + return hproc + + def stop_server(self, hproc): + win32process.TerminateProcess(hproc, 0) + + + def suite(): + return unittest.makeSuite(SSLWinClientTestCase) + + def zap_servers(): + pass + + + if __name__ == '__main__': + try: + if find_openssl() is not None: + Rand.load_file('randpool.dat', -1) + unittest.TextTestRunner().run(suite()) + Rand.save_file('randpool.dat') + finally: + zap_servers() diff --git a/tests/test_threading.py b/tests/test_threading.py new file mode 100644 index 0000000..7912a61 --- /dev/null +++ b/tests/test_threading.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python + +"""Unit tests for M2Crypto.threading. + +Copyright (C) 2007 Open Source Applications Foundation. All Rights Reserved. +""" + +import unittest +from M2Crypto import threading as m2threading, Rand + + +class ThreadingTestCase(unittest.TestCase): + + def setUp(self): + m2threading.init() + + def tearDown(self): + m2threading.cleanup() + + def test_pass(self): + pass + + def test_multiInitCleanup(self): + m2threading.init() + m2threading.init() + m2threading.cleanup() + m2threading.cleanup() + + m2threading.init() + m2threading.cleanup() + + +def suite(): + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(ThreadingTestCase)) + return suite + + +if __name__ == '__main__': + Rand.load_file('randpool.dat', -1) + unittest.TextTestRunner().run(suite()) + Rand.save_file('randpool.dat') diff --git a/tests/test_x509.py b/tests/test_x509.py new file mode 100644 index 0000000..7ea86df --- /dev/null +++ b/tests/test_x509.py @@ -0,0 +1,559 @@ +#!/usr/bin/env python + +"""Unit tests for M2Crypto.X509. + +Contributed by Toby Allsopp <toby@MI6.GEN.NZ> under M2Crypto's license. + +Portions created by Open Source Applications Foundation (OSAF) are +Copyright (C) 2004-2005 OSAF. All Rights Reserved. +Author: Heikki Toivonen +""" + +import unittest +import os, time, base64, sys +from M2Crypto import X509, EVP, RSA, Rand, ASN1, m2, util, BIO + +class X509TestCase(unittest.TestCase): + + def callback(self, *args): + pass + + def mkreq(self, bits, ca=0): + pk = EVP.PKey() + x = X509.Request() + rsa = RSA.gen_key(bits, 65537, self.callback) + pk.assign_rsa(rsa) + rsa = None # should not be freed here + x.set_pubkey(pk) + name = x.get_subject() + name.C = "UK" + name.CN = "OpenSSL Group" + if not ca: + ext1 = X509.new_extension('subjectAltName', 'DNS:foobar.example.com') + ext2 = X509.new_extension('nsComment', 'Hello there') + extstack = X509.X509_Extension_Stack() + extstack.push(ext1) + extstack.push(ext2) + x.add_extensions(extstack) + self.assertRaises(ValueError, x.sign, pk, 'sha513') + x.sign(pk,'sha1') + assert x.verify(pk) + pk2 = x.get_pubkey() + assert x.verify(pk2) + return x, pk + + def test_ext(self): + self.assertRaises(ValueError, X509.new_extension, + 'subjectKeyIdentifier', 'hash') + ext = X509.new_extension('subjectAltName', 'DNS:foobar.example.com') + assert ext.get_value() == 'DNS:foobar.example.com' + assert ext.get_value(indent=2) == ' DNS:foobar.example.com' + assert ext.get_value(flag=m2.X509V3_EXT_PARSE_UNKNOWN) == 'DNS:foobar.example.com' + + def test_extstack(self): + # new + ext1 = X509.new_extension('subjectAltName', 'DNS:foobar.example.com') + ext2 = X509.new_extension('nsComment', 'Hello there') + extstack = X509.X509_Extension_Stack() + + # push + extstack.push(ext1) + extstack.push(ext2) + assert(extstack[1].get_name() == 'nsComment') + assert len(extstack) == 2 + + # iterator + i = 0 + for e in extstack: + i += 1 + assert len(e.get_name()) > 0 + assert i == 2 + + # pop + ext3 = extstack.pop() + assert len(extstack) == 1 + assert(extstack[0].get_name() == 'subjectAltName') + extstack.push(ext3) + assert len(extstack) == 2 + assert(extstack[1].get_name() == 'nsComment') + + assert extstack.pop() is not None + assert extstack.pop() is not None + assert extstack.pop() is None + + def test_x509_name(self): + n = X509.X509_Name() + n.C = 'US' # It seems this actually needs to be a real 2 letter country code + assert n.C == 'US' + n.SP = 'State or Province' + assert n.SP == 'State or Province' + n.L = 'locality name' + assert n.L == 'locality name' + n.O = 'orhanization name' + assert n.O == 'orhanization name' + n.OU = 'org unit' + assert n.OU == 'org unit' + n.CN = 'common name' + assert n.CN == 'common name' + n.Email = 'bob@example.com' + assert n.Email == 'bob@example.com' + n.serialNumber = '1234' + assert n.serialNumber == '1234' + n.SN = 'surname' + assert n.SN == 'surname' + n.GN = 'given name' + assert n.GN == 'given name' + assert n.as_text() == 'C=US, ST=State or Province, L=locality name, O=orhanization name, OU=org unit, CN=common name/emailAddress=bob@example.com/serialNumber=1234, SN=surname, GN=given name', '"%s"' % n.as_text() + assert len(n) == 10, len(n) + n.givenName = 'name given' + assert n.GN == 'given name' # Just gets the first + assert n.as_text() == 'C=US, ST=State or Province, L=locality name, O=orhanization name, OU=org unit, CN=common name/emailAddress=bob@example.com/serialNumber=1234, SN=surname, GN=given name, GN=name given', '"%s"' % n.as_text() + assert len(n) == 11, len(n) + n.add_entry_by_txt(field="CN", type=ASN1.MBSTRING_ASC, + entry="Proxy", len=-1, loc=-1, set=0) + assert len(n) == 12, len(n) + assert n.entry_count() == 12, n.entry_count() + assert n.as_text() == 'C=US, ST=State or Province, L=locality name, O=orhanization name, OU=org unit, CN=common name/emailAddress=bob@example.com/serialNumber=1234, SN=surname, GN=given name, GN=name given, CN=Proxy', '"%s"' % n.as_text() + + self.assertRaises(AttributeError, n.__getattr__, 'foobar') + n.foobar = 1 + assert n.foobar == 1, n.foobar + + # X509_Name_Entry tests + l = 0 + for entry in n: + assert isinstance(entry, X509.X509_Name_Entry), entry + assert isinstance(entry.get_object(), ASN1.ASN1_Object), entry + assert isinstance(entry.get_data(), ASN1.ASN1_String), entry + l += 1 + assert l == 12, l + + l = 0 + for cn in n.get_entries_by_nid(m2.NID_commonName): + assert isinstance(cn, X509.X509_Name_Entry), cn + assert isinstance(cn.get_object(), ASN1.ASN1_Object), cn + data = cn.get_data() + assert isinstance(data, ASN1.ASN1_String), data + t = data.as_text() + assert t == "common name" or t == "Proxy", t + l += 1 + assert l == 2, l + + cn.set_data("Hello There!") + assert cn.get_data().as_text() == "Hello There!", cn.get_data().as_text() + + assert n.as_hash() == 1697185131 + + self.assertRaises(IndexError, lambda: n[100]) + self.assert_(n[10]) + + def test_mkreq(self): + (req, _) = self.mkreq(1024) + req.save_pem('tests/tmp_request.pem') + req2 = X509.load_request('tests/tmp_request.pem') + os.remove('tests/tmp_request.pem') + req.save('tests/tmp_request.pem') + req3 = X509.load_request('tests/tmp_request.pem') + os.remove('tests/tmp_request.pem') + req.save('tests/tmp_request.der', format=X509.FORMAT_DER) + req4 = X509.load_request('tests/tmp_request.der', + format=X509.FORMAT_DER) + os.remove('tests/tmp_request.der') + assert req.as_pem() == req2.as_pem() + assert req.as_text() == req2.as_text() + assert req.as_der() == req2.as_der() + assert req.as_pem() == req3.as_pem() + assert req.as_text() == req3.as_text() + assert req.as_der() == req3.as_der() + assert req.as_pem() == req4.as_pem() + assert req.as_text() == req4.as_text() + assert req.as_der() == req4.as_der() + self.assertEqual(req.get_version(), 0) + req.set_version(1) + self.assertEqual(req.get_version(), 1) + req.set_version(0) + self.assertEqual(req.get_version(), 0) + + + def test_mkcert(self): + req, pk = self.mkreq(1024) + pkey = req.get_pubkey() + assert(req.verify(pkey)) + sub = req.get_subject() + assert len(sub) == 2, len(sub) + cert = X509.X509() + cert.set_serial_number(1) + cert.set_version(2) + cert.set_subject(sub) + t = long(time.time()) + time.timezone + now = ASN1.ASN1_UTCTIME() + now.set_time(t) + nowPlusYear = ASN1.ASN1_UTCTIME() + nowPlusYear.set_time(t + 60 * 60 * 24 * 365) + cert.set_not_before(now) + cert.set_not_after(nowPlusYear) + assert str(cert.get_not_before()) == str(now) + assert str(cert.get_not_after()) == str(nowPlusYear) + issuer = X509.X509_Name() + issuer.CN = 'The Issuer Monkey' + issuer.O = 'The Organization Otherwise Known as My CA, Inc.' + cert.set_issuer(issuer) + cert.set_pubkey(pkey) + cert.set_pubkey(cert.get_pubkey()) # Make sure get/set work + ext = X509.new_extension('subjectAltName', 'DNS:foobar.example.com') + ext.set_critical(0) + assert ext.get_critical() == 0 + cert.add_ext(ext) + cert.sign(pk, 'sha1') + self.assertRaises(ValueError, cert.sign, pk, 'nosuchalgo') + assert(cert.get_ext('subjectAltName').get_name() == 'subjectAltName') + assert(cert.get_ext_at(0).get_name() == 'subjectAltName') + assert(cert.get_ext_at(0).get_value() == 'DNS:foobar.example.com') + assert cert.get_ext_count() == 1, cert.get_ext_count() + self.assertRaises(IndexError, cert.get_ext_at, 1) + assert cert.verify() + assert cert.verify(pkey) + assert cert.verify(cert.get_pubkey()) + assert cert.get_version() == 2 + assert cert.get_serial_number() == 1 + assert cert.get_issuer().CN == 'The Issuer Monkey' + + if m2.OPENSSL_VERSION_NUMBER >= 0x90800f: + assert not cert.check_ca() + assert not cert.check_purpose(m2.X509_PURPOSE_SSL_SERVER, 1) + assert not cert.check_purpose(m2.X509_PURPOSE_NS_SSL_SERVER, 1) + assert cert.check_purpose(m2.X509_PURPOSE_SSL_SERVER, 0) + assert cert.check_purpose(m2.X509_PURPOSE_NS_SSL_SERVER, 0) + assert cert.check_purpose(m2.X509_PURPOSE_ANY, 0) + else: + self.assertRaises(AttributeError, cert.check_ca) + + def mkcacert(self): + req, pk = self.mkreq(1024, ca=1) + pkey = req.get_pubkey() + sub = req.get_subject() + cert = X509.X509() + cert.set_serial_number(1) + cert.set_version(2) + cert.set_subject(sub) + t = long(time.time()) + time.timezone + now = ASN1.ASN1_UTCTIME() + now.set_time(t) + nowPlusYear = ASN1.ASN1_UTCTIME() + nowPlusYear.set_time(t + 60 * 60 * 24 * 365) + cert.set_not_before(now) + cert.set_not_after(nowPlusYear) + issuer = X509.X509_Name() + issuer.C = "UK" + issuer.CN = "OpenSSL Group" + cert.set_issuer(issuer) + cert.set_pubkey(pkey) + ext = X509.new_extension('basicConstraints', 'CA:TRUE') + cert.add_ext(ext) + cert.sign(pk, 'sha1') + + if m2.OPENSSL_VERSION_NUMBER >= 0x0090800fL: + assert cert.check_ca() + assert cert.check_purpose(m2.X509_PURPOSE_SSL_SERVER, 1) + assert cert.check_purpose(m2.X509_PURPOSE_NS_SSL_SERVER, 1) + assert cert.check_purpose(m2.X509_PURPOSE_ANY, 1) + assert cert.check_purpose(m2.X509_PURPOSE_SSL_SERVER, 0) + assert cert.check_purpose(m2.X509_PURPOSE_NS_SSL_SERVER, 0) + assert cert.check_purpose(m2.X509_PURPOSE_ANY, 0) + else: + self.assertRaises(AttributeError, cert.check_ca) + + return cert, pk, pkey + + def test_mkcacert(self): + cacert, pk, pkey = self.mkcacert() + assert cacert.verify(pkey) + + + def test_mkproxycert(self): + cacert, pk1, pkey = self.mkcacert() + end_entity_cert_req, pk2 = self.mkreq(1024) + end_entity_cert = self.make_eecert(cacert) + end_entity_cert.set_subject(end_entity_cert_req.get_subject()) + end_entity_cert.set_pubkey(end_entity_cert_req.get_pubkey()) + end_entity_cert.sign(pk1, 'sha1') + proxycert = self.make_proxycert(end_entity_cert) + proxycert.sign(pk2, 'sha1') + assert proxycert.verify(pk2) + assert proxycert.get_ext_at(0).get_name() == 'proxyCertInfo', proxycert.get_ext_at(0).get_name() + assert proxycert.get_ext_at(0).get_value() == 'Path Length Constraint: infinite\nPolicy Language: Inherit all\n', '"%s"' % proxycert.get_ext_at(0).get_value() + assert proxycert.get_ext_count() == 1, proxycert.get_ext_count() + assert proxycert.get_subject().as_text() == 'C=UK, CN=OpenSSL Group, CN=Proxy', proxycert.get_subject().as_text() + assert proxycert.get_subject().as_text(indent=2, flags=m2.XN_FLAG_RFC2253) == ' CN=Proxy,CN=OpenSSL Group,C=UK', '"%s"' % proxycert.get_subject().as_text(indent=2, flags=m2.XN_FLAG_RFC2253) + + def make_eecert(self, cacert): + eecert = X509.X509() + eecert.set_serial_number(2) + eecert.set_version(2) + t = long(time.time()) + time.timezone + now = ASN1.ASN1_UTCTIME() + now.set_time(t) + now_plus_year = ASN1.ASN1_UTCTIME() + now_plus_year.set_time(t + 60 * 60 * 24 * 365) + eecert.set_not_before(now) + eecert.set_not_after(now_plus_year) + eecert.set_issuer(cacert.get_subject()) + return eecert + + def make_proxycert(self, eecert): + proxycert = X509.X509() + pk2 = EVP.PKey() + proxykey = RSA.gen_key(1024, 65537, self.callback) + pk2.assign_rsa(proxykey) + proxycert.set_pubkey(pk2) + proxycert.set_version(2) + not_before = ASN1.ASN1_UTCTIME() + not_after = ASN1.ASN1_UTCTIME() + not_before.set_time(int(time.time())) + offset = 12 * 3600 + not_after.set_time(int(time.time()) + offset ) + proxycert.set_not_before(not_before) + proxycert.set_not_after(not_after) + proxycert.set_issuer_name(eecert.get_subject()) + proxycert.set_serial_number(12345678) + proxy_subject_name = X509.X509_Name() + issuer_name_string = eecert.get_subject().as_text() + seq = issuer_name_string.split(",") + + subject_name = X509.X509_Name() + for entry in seq: + l = entry.split("=") + subject_name.add_entry_by_txt(field=l[0].strip(), + type=ASN1.MBSTRING_ASC, + entry=l[1], len=-1, loc=-1, set=0) + + subject_name.add_entry_by_txt(field="CN", type=ASN1.MBSTRING_ASC, + entry="Proxy", len=-1, loc=-1, set=0) + + + proxycert.set_subject_name(subject_name) + pci_ext = X509.new_extension("proxyCertInfo", + "critical,language:Inherit all", 1) # XXX leaks 8 bytes + proxycert.add_ext(pci_ext) + return proxycert + + def test_fingerprint(self): + x509 = X509.load_cert('tests/x509.pem') + fp = x509.get_fingerprint('sha1') + expected = '8D2EB9E203B5FFDC7F4FA7DC4103E852A55B808D' + assert fp == expected, '%s != %s' % (fp, expected) + + def test_load_der_string(self): + f = open('tests/x509.der', 'rb') + x509 = X509.load_cert_der_string(''.join(f.readlines())) + fp = x509.get_fingerprint('sha1') + expected = '8D2EB9E203B5FFDC7F4FA7DC4103E852A55B808D' + assert fp == expected, '%s != %s' % (fp, expected) + + def test_save_der_string(self): + x509 = X509.load_cert('tests/x509.pem') + s = x509.as_der() + f = open('tests/x509.der', 'rb') + s2 = f.read() + f.close() + assert s == s2 + + def test_load(self): + x509 = X509.load_cert('tests/x509.pem') + x5092 = X509.load_cert('tests/x509.der', format=X509.FORMAT_DER) + assert x509.as_text() == x5092.as_text() + assert x509.as_pem() == x5092.as_pem() + assert x509.as_der() == x5092.as_der() + return + + def test_load_bio(self): + bio = BIO.openfile('tests/x509.pem') + bio2 = BIO.openfile('tests/x509.der') + x509 = X509.load_cert_bio(bio) + x5092 = X509.load_cert_bio(bio2, format=X509.FORMAT_DER) + + self.assertRaises(ValueError, X509.load_cert_bio, bio2, format=45678) + + assert x509.as_text() == x5092.as_text() + assert x509.as_pem() == x5092.as_pem() + assert x509.as_der() == x5092.as_der() + return + + def test_load_string(self): + f = open('tests/x509.pem') + s = f.read() + f.close() + f2 = open('tests/x509.der', 'rb') + s2 = f2.read() + f2.close() + x509 = X509.load_cert_string(s) + x5092 = X509.load_cert_string(s2, X509.FORMAT_DER) + assert x509.as_text() == x5092.as_text() + assert x509.as_pem() == x5092.as_pem() + assert x509.as_der() == x5092.as_der() + return + + def test_load_request_bio(self): + (req, _) = self.mkreq(512) + + r1 = X509.load_request_der_string(req.as_der()) + r2 = X509.load_request_string(req.as_der(), X509.FORMAT_DER) + r3 = X509.load_request_string(req.as_pem(), X509.FORMAT_PEM) + + r4 = X509.load_request_bio(BIO.MemoryBuffer(req.as_der()), X509.FORMAT_DER) + r5 = X509.load_request_bio(BIO.MemoryBuffer(req.as_pem()), X509.FORMAT_PEM) + + for r in [r1, r2, r3, r4, r5]: + assert req.as_der() == r.as_der() + + self.assertRaises(ValueError, X509.load_request_bio, BIO.MemoryBuffer(req.as_pem()), 345678) + + def test_save(self): + x509 = X509.load_cert('tests/x509.pem') + f = open('tests/x509.pem', 'r') + lTmp = f.readlines() + x509_pem = ''.join(lTmp[44:60]) # -----BEGIN CERTIFICATE----- : -----END CERTIFICATE----- + f.close() + f = open('tests/x509.der', 'rb') + x509_der = f.read() + f.close() + x509.save('tests/tmpcert.pem') + f = open('tests/tmpcert.pem') + s = f.read() + f.close() + self.assertEquals(s, x509_pem) + os.remove('tests/tmpcert.pem') + x509.save('tests/tmpcert.der', format=X509.FORMAT_DER) + f = open('tests/tmpcert.der', 'rb') + s = f.read() + f.close() + self.assertEquals(s, x509_der) + os.remove('tests/tmpcert.der') + + def test_malformed_data(self): + self.assertRaises(X509.X509Error, X509.load_cert_string, 'Hello') + self.assertRaises(X509.X509Error, X509.load_cert_der_string, 'Hello') + self.assertRaises(X509.X509Error, X509.new_stack_from_der, 'Hello') + self.assertRaises(X509.X509Error, X509.load_cert, 'tests/alltests.py') + self.assertRaises(X509.X509Error, X509.load_request, 'tests/alltests.py') + self.assertRaises(X509.X509Error, X509.load_request_string, 'Hello') + self.assertRaises(X509.X509Error, X509.load_request_der_string, 'Hello') + self.assertRaises(X509.X509Error, X509.load_crl, 'tests/alltests.py') + + def test_long_serial(self): + from M2Crypto import X509 + cert = X509.load_cert('tests/long_serial_cert.pem') + self.assertEquals(cert.get_serial_number(), 17616841808974579194) + + cert = X509.load_cert('tests/thawte.pem') + self.assertEquals(cert.get_serial_number(), 127614157056681299805556476275995414779) + + +class X509_StackTestCase(unittest.TestCase): + + def test_make_stack_from_der(self): + f = open("tests/der_encoded_seq.b64") + b64 = f.read(1304) + seq = base64.decodestring(b64) + stack = X509.new_stack_from_der(seq) + cert = stack.pop() + assert stack.pop() is None + + cert.foobar = 1 + assert cert.foobar == 1 + + subject = cert.get_subject() + assert str(subject) == "/DC=org/DC=doegrids/OU=Services/CN=host/bosshog.lbl.gov" + + def test_make_stack_check_num(self): + f = open("tests/der_encoded_seq.b64") + b64 = f.read(1304) + seq = base64.decodestring(b64) + stack = X509.new_stack_from_der(seq) + num = len(stack) + assert num == 1 + cert = stack.pop() + num = len(stack) + assert num == 0 + subject = cert.get_subject() + assert str(subject) == "/DC=org/DC=doegrids/OU=Services/CN=host/bosshog.lbl.gov" + + def test_make_stack(self): + stack = X509.X509_Stack() + cert = X509.load_cert("tests/x509.pem") + issuer = X509.load_cert("tests/ca.pem") + cert_subject1 = cert.get_subject() + issuer_subject1 = issuer.get_subject() + stack.push(cert) + stack.push(issuer) + + # Test stack iterator + i = 0 + for c in stack: + i += 1 + assert len(c.get_subject().CN) > 0 + assert i == 2 + + issuer_pop = stack.pop() + cert_pop = stack.pop() + cert_subject2 = cert_pop.get_subject() + issuer_subject2 = issuer.get_subject() + assert str(cert_subject1) == str(cert_subject2) + assert str(issuer_subject1) == str(issuer_subject2) + + def test_as_der(self): + stack = X509.X509_Stack() + cert = X509.load_cert("tests/x509.pem") + issuer = X509.load_cert("tests/ca.pem") + cert_subject1 = cert.get_subject() + issuer_subject1 = issuer.get_subject() + stack.push(cert) + stack.push(issuer) + der_seq = stack.as_der() + stack2 = X509.new_stack_from_der(der_seq) + issuer_pop = stack2.pop() + cert_pop = stack2.pop() + cert_subject2 = cert_pop.get_subject() + issuer_subject2 = issuer.get_subject() + assert str(cert_subject1) == str(cert_subject2) + assert str(issuer_subject1) == str(issuer_subject2) + + +class X509_ExtTestCase(unittest.TestCase): + + def test_ext(self): + if 0: # XXX + # With this leaks 8 bytes: + name = "proxyCertInfo" + value = "critical,language:Inherit all" + else: + # With this there are no leaks: + name = "nsComment" + value = "Hello" + + lhash = m2.x509v3_lhash() + ctx = m2.x509v3_set_conf_lhash(lhash) + x509_ext_ptr = m2.x509v3_ext_conf(lhash, ctx, name, value) + x509_ext = X509.X509_Extension(x509_ext_ptr, 1) + + +class CRLTestCase(unittest.TestCase): + def test_new(self): + crl = X509.CRL() + self.assertEqual(crl.as_text()[:34], + 'Certificate Revocation List (CRL):') + + +def suite(): + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(X509TestCase)) + suite.addTest(unittest.makeSuite(X509_StackTestCase)) + suite.addTest(unittest.makeSuite(X509_ExtTestCase)) + suite.addTest(unittest.makeSuite(CRLTestCase)) + return suite + + +if __name__ == '__main__': + Rand.load_file('randpool.dat', -1) + unittest.TextTestRunner().run(suite()) + Rand.save_file('randpool.dat') diff --git a/tests/thawte.pem b/tests/thawte.pem new file mode 100644 index 0000000..34af29e --- /dev/null +++ b/tests/thawte.pem @@ -0,0 +1,25 @@ +-----BEGIN CERTIFICATE-----
+MIIEKjCCAxKgAwIBAgIQYAGXt0an6rS0mtZLL/eQ+zANBgkqhkiG9w0BAQsFADCB
+rjELMAkGA1UEBhMCVVMxFTATBgNVBAoTDHRoYXd0ZSwgSW5jLjEoMCYGA1UECxMf
+Q2VydGlmaWNhdGlvbiBTZXJ2aWNlcyBEaXZpc2lvbjE4MDYGA1UECxMvKGMpIDIw
+MDggdGhhd3RlLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxJDAiBgNV
+BAMTG3RoYXd0ZSBQcmltYXJ5IFJvb3QgQ0EgLSBHMzAeFw0wODA0MDIwMDAwMDBa
+Fw0zNzEyMDEyMzU5NTlaMIGuMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMdGhhd3Rl
+LCBJbmMuMSgwJgYDVQQLEx9DZXJ0aWZpY2F0aW9uIFNlcnZpY2VzIERpdmlzaW9u
+MTgwNgYDVQQLEy8oYykgMjAwOCB0aGF3dGUsIEluYy4gLSBGb3IgYXV0aG9yaXpl
+ZCB1c2Ugb25seTEkMCIGA1UEAxMbdGhhd3RlIFByaW1hcnkgUm9vdCBDQSAtIEcz
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsr8nLPvb2FvdeHsbnndm
+gcs+vHyu86YnmjSjaDFxODNi5PNxZnmxqWWjpYvVj2AtP0LMqmsywCPLLEHd5N/8
+YZzic7IilRFDGF/Eth9XbAoFWCLINkw6fKXRz4aviKdEAhN0cXMKQlkC+BsUa0Lf
+b1+6a4KinVvnSr0eAXLbS3ToO39/fR8EtCab4LRarEc9VbjXsCZSKAExQGbY2SS9
+9irY7CFJXJv2eul/VTV+lmuNk5Mny5K76qxAwJ/C+IDPXfRa3M50hqY+bAtTyr2S
+zhkGcuYMXDhpxwTWvGzOW/b3aJzcJRVIiKHpqfiYnODz1TEoYRFsZ5aNOZnLwkUk
+OQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNV
+HQ4EFgQUrWyqlGCc7eT/+j4KdCtjA/e2Wb8wDQYJKoZIhvcNAQELBQADggEBABpA
+2JVlrAmSicY59BDlqQ5mU1143vokkbvnRFHfxhY0Cu9qRFHqKweKA3rD6z8KLFIW
+oCtDuSWQP3CpMyVtRRooOyfPqsMpQhvfO0zAMzRbQYi/aytlryjvsvXDqmbOe1bu
+t8jLZ8HJnBoYuMTDSQPxYA5QzUbF83d597YV4Djbxy8ooAw/dyZ02SUS2jHaGh7c
+KUGRIjxpp7sC8rZcJwOJ9Abqm+RyguOhCcHpABnTPtRwa7pxpqpYrvS76Wy274fM
+m7v/OeZWYdMKp8RcTGB7BXcmer/YB1IsYvdwY9k5vG8cwnncdimvzsUsZAReiDZu
+MdRAGmI0Nj81Aa6sY6A=
+-----END CERTIFICATE-----
diff --git a/tests/x509.der b/tests/x509.der Binary files differnew file mode 100644 index 0000000..fbc9e81 --- /dev/null +++ b/tests/x509.der diff --git a/tests/x509.pem b/tests/x509.pem new file mode 100644 index 0000000..4a1f1ee --- /dev/null +++ b/tests/x509.pem @@ -0,0 +1,75 @@ +Certificate: + Data: + Version: 3 (0x2) + Serial Number: + d1:b6:bf:af:06:17:8c:bf + Signature Algorithm: sha1WithRSAEncryption + Issuer: C=US, ST=California, O=M2Crypto, CN=Heikki Toivonen + Validity + Not Before: Jul 28 04:34:34 2009 GMT + Not After : Jul 26 04:34:34 2019 GMT + Subject: C=US, ST=California, O=M2Crypto, CN=X509 + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + RSA Public Key: (1024 bit) + Modulus (1024 bit): + 00:d3:62:55:12:30:b8:dc:84:7c:63:bd:80:1d:19: + 1a:72:f2:28:f8:59:0b:2a:6b:f2:2a:23:9d:bb:0f: + 7f:92:5e:dd:27:74:bc:78:0a:27:ab:1c:2e:23:1c: + 26:77:48:b6:8f:03:ef:57:1c:a0:54:ae:1a:e8:f5: + 24:a1:46:a1:27:48:55:33:98:fc:db:6a:83:2e:89: + 3f:e0:f3:91:9d:da:4f:db:74:90:9d:a6:8d:4a:46: + cb:9f:ba:b8:60:df:ae:ee:22:4b:3f:80:55:f7:1d: + 89:3c:2b:28:df:46:19:d5:18:ac:e9:07:4e:40:81: + 75:bc:da:5b:d5:e1:c2:04:15 + Exponent: 65537 (0x10001) + X509v3 extensions: + X509v3 Basic Constraints: + CA:FALSE + Netscape Comment: + OpenSSL Generated Certificate + X509v3 Subject Key Identifier: + B1:C4:6F:98:6F:E8:3B:8C:A1:26:11:81:97:9A:12:50:4A:1A:6C:88 + X509v3 Authority Key Identifier: + keyid:AD:64:45:74:8F:83:C7:2C:D5:D7:A0:85:91:10:40:9A:9C:96:CF:EE + + Signature Algorithm: sha1WithRSAEncryption + 3f:0b:44:bc:d2:da:5f:a9:39:be:08:53:e6:fd:10:ff:d6:f0: + a3:51:f6:be:03:20:cc:b3:52:cf:0f:7c:3f:56:42:6f:9d:72: + 9b:09:a5:64:3f:43:29:24:2b:d6:79:94:54:2f:99:e8:ce:fe: + fd:de:bb:ca:43:28:16:ff:32:ac:3d:c5:56:db:87:23:3c:d4: + 69:f7:4e:1b:c4:be:c9:d8:27:99:2a:64:be:3a:6b:7e:51:85: + db:75:35:40:a5:6c:ae:53:c3:09:e7:00:35:17:64:1a:17:71: + c5:d5:59:e5:8f:fc:96:4a:f9:81:33:23:4c:c1:60:71:93:18: + 0a:c4 +-----BEGIN CERTIFICATE----- +MIICjDCCAfWgAwIBAgIJANG2v68GF4y/MA0GCSqGSIb3DQEBBQUAME8xCzAJBgNV +BAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMREwDwYDVQQKEwhNMkNyeXB0bzEY +MBYGA1UEAxMPSGVpa2tpIFRvaXZvbmVuMB4XDTA5MDcyODA0MzQzNFoXDTE5MDcy +NjA0MzQzNFowRDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExETAP +BgNVBAoTCE0yQ3J5cHRvMQ0wCwYDVQQDEwRYNTA5MIGfMA0GCSqGSIb3DQEBAQUA +A4GNADCBiQKBgQDTYlUSMLjchHxjvYAdGRpy8ij4WQsqa/IqI527D3+SXt0ndLx4 +CierHC4jHCZ3SLaPA+9XHKBUrhro9SShRqEnSFUzmPzbaoMuiT/g85Gd2k/bdJCd +po1KRsufurhg367uIks/gFX3HYk8KyjfRhnVGKzpB05AgXW82lvV4cIEFQIDAQAB +o3sweTAJBgNVHRMEAjAAMCwGCWCGSAGG+EIBDQQfFh1PcGVuU1NMIEdlbmVyYXRl +ZCBDZXJ0aWZpY2F0ZTAdBgNVHQ4EFgQUscRvmG/oO4yhJhGBl5oSUEoabIgwHwYD +VR0jBBgwFoAUrWRFdI+DxyzV16CFkRBAmpyWz+4wDQYJKoZIhvcNAQEFBQADgYEA +PwtEvNLaX6k5vghT5v0Q/9bwo1H2vgMgzLNSzw98P1ZCb51ymwmlZD9DKSQr1nmU +VC+Z6M7+/d67ykMoFv8yrD3FVtuHIzzUafdOG8S+ydgnmSpkvjprflGF23U1QKVs +rlPDCecANRdkGhdxxdVZ5Y/8lkr5gTMjTMFgcZMYCsQ= +-----END CERTIFICATE----- +-----BEGIN RSA PRIVATE KEY----- +MIICXQIBAAKBgQDTYlUSMLjchHxjvYAdGRpy8ij4WQsqa/IqI527D3+SXt0ndLx4 +CierHC4jHCZ3SLaPA+9XHKBUrhro9SShRqEnSFUzmPzbaoMuiT/g85Gd2k/bdJCd +po1KRsufurhg367uIks/gFX3HYk8KyjfRhnVGKzpB05AgXW82lvV4cIEFQIDAQAB +AoGATPipcY48QlAb21XNqMrTTrfPI1+JKVFVRPLjJJJoKaxRa2SenDdWaoBAbJh7 +iUP49erA5D+QQkWDlwBs7i0B0NqSkZAUVTfzRjGackTNJUQ+smfeqRLMH+Oru6DS +VFbb818nJOJKqMMhMz8SrPrrbg+qiHlJ3JUQnNzTYohOMAECQQDvTJBSSit34ZBO +ABj4vWYucCnOygcpICQnIsG97sZmF8tuF55tA5e+0v9R7BPuyAjrQnKJqDj3r/AY +AxhgngGVAkEA4iMGoHzoSQvh+gT0A2rPCtVo+URNswIEZhQmMuA0VjrFCphWkZE+ +3jgDsJTNQUJs4mczQMcBzL34Nh1cJThYgQJARMMrdXn6o6gdX0yH4HIMOqvgV5uW +Eys5OEW0hm9mc0/DFQ+UZp7xq9PVqiS8VZEFfxTI9OVx+TqFM2EwUBMXQQJBAIge +n0mRhl0Z6v+NZbh83X3e8h5BUCf1ieJMNKYhMT/KhnsXMdzTui0XOJldKKQksNgj +WMWgROQSYctpJuM8pIECQQCNN27XVHs4YAQ6GvBkrHsK5w6LZkm6UaJgbCqDqyeS +eqfPp9VRurZ/FhK1mPbgNN67U4Ik1nwjR0o8wD4mreIj +-----END RSA PRIVATE KEY----- |