use NSS for SSL operations

This commit is contained in:
John Dennis
2010-05-31 07:40:17 -04:00
committed by Rob Crittenden
parent 1dd7b11b0b
commit 31027c6183
7 changed files with 175 additions and 434 deletions

View File

@@ -178,7 +178,7 @@ Requires: python-kerberos >= 1.1-3
Requires: authconfig
Requires: gnupg
Requires: pyOpenSSL
Requires: python-nss >= 0.8
Requires: python-nss >= 0.9
Requires: python-lxml
%description python

View File

@@ -32,7 +32,6 @@ Also see the `ipaserver.rpcserver` module.
from types import NoneType
import threading
import socket
import os
import errno
from xmlrpclib import Binary, Fault, dumps, loads, ServerProxy, Transport, ProtocolError
@@ -42,15 +41,9 @@ from ipalib.errors import public_errors, PublicError, UnknownError, NetworkError
from ipalib import errors
from ipalib.request import context
from ipapython import ipautil
from OpenSSL import SSL
import httplib
try:
from httplib import SSLFile
from httplib import FakeSocket
except ImportError:
from ipapython.ipasslfile import SSLFile
from ipapython.ipasslfile import FakeSocket
from ipapython.nsslib import NSSHTTPS
from nss.error import NSPRError
# Some Kerberos error definitions from krb5.h
KRB5KDC_ERR_S_PRINCIPAL_UNKNOWN = (-1765328377L)
@@ -199,121 +192,9 @@ class SSLTransport(Transport):
def make_connection(self, host):
host, extra_headers, x509 = self.get_host_info(host)
return SSLSocket(host, None, **(x509 or {}))
class SSLFile(SSLFile):
"""
Override the _read method so we can handle PyOpenSSL errors
gracefully.
"""
def _read(self):
buf = ''
while True:
try:
buf = self._ssl.read(self._bufsize)
except SSL.ZeroReturnError:
# Nothing more to be read
break
except SSL.SysCallError, e:
print "SSL exception", e.args
break
except SSL.WantWriteError:
break
except SSL.WantReadError:
break
except socket.error, err:
if err[0] == errno.EINTR:
continue
if err[0] == errno.EBADF:
# XXX socket was closed?
break
raise
else:
break
return buf
class FakeSocket(FakeSocket):
"""
Override this class so we can end up using our own SSLFile
implementation.
"""
def makefile(self, mode, bufsize=None):
if mode != 'r' and mode != 'rb':
raise httplib.UnimplementedFileMode()
return SSLFile(self._shared, self._ssl, bufsize)
class SSLConnection(httplib.HTTPConnection):
"""
Use OpenSSL as the SSL provider instead of the built-in python SSL
support. The built-in SSL client doesn't do CA validation.
By default we will attempt to load the ca-bundle.crt and our own
IPA CA for validation purposes. To add an additional CA to verify
against set the x509['ca_file'] to the path of the CA PEM file in
KerbTransport.get_host_info
"""
default_port = httplib.HTTPSConnection.default_port
def verify_callback(self, conn, cert, errnum, depth, ok):
"""
Verify callback. If we get here then the certificate is ok.
"""
return ok
def __init__(self, host, port=None, key_file=None, cert_file=None,
ca_file=None, strict=None):
httplib.HTTPConnection.__init__(self, host, port, strict)
self.key_file = key_file
self.cert_file = cert_file
self.ca_file = ca_file
def connect(self):
ctx = SSL.Context(SSL.SSLv23_METHOD)
ctx.set_verify(SSL.VERIFY_PEER, self.verify_callback)
if self.key_file:
ctx.use_privatekey_file (self.key_file)
if self.cert_file:
ctx.use_certificate_file(self.cert_file)
if os.path.exists("/etc/pki/tls/certs/ca-bundle.crt"):
ctx.load_verify_locations("/etc/pki/tls/certs/ca-bundle.crt")
if os.path.exists("/etc/ipa/ca.crt"):
ctx.load_verify_locations("/etc/ipa/ca.crt")
if self.ca_file is not None and os.path.exists(self.ca_file):
ctx.load_verify_locations(self.ca_file)
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
ssl = SSL.Connection(ctx, sock)
ssl.connect((self.host, self.port))
ssl.do_handshake()
self.sock = FakeSocket(sock, ssl)
class SSLSocket(httplib.HTTP):
"""
This is more or less equivalent to the httplib.HTTPS class, we juse
use our own connection provider.
"""
_connection_class = SSLConnection
def __init__(self, host='', port=None, key_file=None, cert_file=None,
ca_file=None, strict=None):
# provide a default host, pass the X509 cert info
# urf. compensate for bad input.
if port == 0:
port = None
self._setup(self._connection_class(host, port, key_file,
cert_file, ca_file, strict))
# we never actually use these for anything, but we keep them
# here for compatibility with post-1.5.2 CVS.
self.key_file = key_file
self.cert_file = cert_file
self.ca_file = ca_file
conn = NSSHTTPS(host, 443, dbdir="/etc/pki/nssdb")
conn.connect()
return conn
class KerbTransport(SSLTransport):
"""
@@ -417,7 +298,7 @@ class xmlclient(Connectible):
error=e.faultString,
server=self.env.xmlrpc_uri,
)
except socket.error, e:
raise NetworkError(uri=self.env.xmlrpc_uri, error=e.args[1])
except NSPRError, e:
raise NetworkError(uri=self.env.xmlrpc_uri, error=str(e))
except ProtocolError, e:
raise NetworkError(uri=self.env.xmlrpc_uri, error=e.errmsg)

View File

@@ -22,9 +22,9 @@ import httplib
import xml.dom.minidom
from ipapython import nsslib
import nss.nss as nss
from nss.error import NSPRError
from ipalib.errors import NetworkError, CertificateOperationError
from urllib import urlencode
import socket
import logging
def get_ca_certchain(ca_host=None):
@@ -76,10 +76,11 @@ def https_request(host, port, url, secdir, password, nickname, **kw):
"Accept": "text/plain"}
try:
conn = nsslib.NSSConnection(host, port, dbdir=secdir)
conn.sslsock.set_client_auth_data_callback(nsslib.client_auth_data_callback,
nickname,
password, nss.get_default_certdb())
conn.sock.set_client_auth_data_callback(nsslib.client_auth_data_callback,
nickname,
password, nss.get_default_certdb())
conn.set_debuglevel(0)
conn.connect()
conn.request("POST", url, post, request_headers)
res = conn.getresponse()
@@ -122,8 +123,8 @@ def http_request(host, port, url, **kw):
http_headers = res.msg.dict
http_body = res.read()
conn.close()
except socket.error, e:
raise NetworkError(uri=uri, error=e.args[1])
except NSPRError, e:
raise NetworkError(uri=uri, error=str(e))
logging.debug('request status %d', http_status)
logging.debug('request reason_phrase %r', http_reason_phrase)

View File

@@ -1,185 +0,0 @@
# This is a forward backport of the Python2.5 uuid module. It isn't available
# in Python 2.6
# The next several classes are used to define FakeSocket, a socket-like
# interface to an SSL connection.
# The primary complexity comes from faking a makefile() method. The
# standard socket makefile() implementation calls dup() on the socket
# file descriptor. As a consequence, clients can call close() on the
# parent socket and its makefile children in any order. The underlying
# socket isn't closed until they are all closed.
# The implementation uses reference counting to keep the socket open
# until the last client calls close(). SharedSocket keeps track of
# the reference counting and SharedSocketClient provides an constructor
# and close() method that call incref() and decref() correctly.
import socket
import errno
from httplib import UnimplementedFileMode, HTTPException
error = HTTPException
class SharedSocket:
def __init__(self, sock):
self.sock = sock
self._refcnt = 0
def incref(self):
self._refcnt += 1
def decref(self):
self._refcnt -= 1
assert self._refcnt >= 0
if self._refcnt == 0:
self.sock.close()
def __del__(self):
self.sock.close()
class SharedSocketClient:
def __init__(self, shared):
self._closed = 0
self._shared = shared
self._shared.incref()
self._sock = shared.sock
def close(self):
if not self._closed:
self._shared.decref()
self._closed = 1
self._shared = None
class SSLFile(SharedSocketClient):
"""File-like object wrapping an SSL socket."""
BUFSIZE = 8192
def __init__(self, sock, ssl, bufsize=None):
SharedSocketClient.__init__(self, sock)
self._ssl = ssl
self._buf = ''
self._bufsize = bufsize or self.__class__.BUFSIZE
def _read(self):
buf = ''
# put in a loop so that we retry on transient errors
while True:
try:
buf = self._ssl.read(self._bufsize)
except socket.sslerror, err:
if (err[0] == socket.SSL_ERROR_WANT_READ
or err[0] == socket.SSL_ERROR_WANT_WRITE):
continue
if (err[0] == socket.SSL_ERROR_ZERO_RETURN
or err[0] == socket.SSL_ERROR_EOF):
break
raise
except socket.error, err:
if err[0] == errno.EINTR:
continue
if err[0] == errno.EBADF:
# XXX socket was closed?
break
raise
else:
break
return buf
def read(self, size=None):
L = [self._buf]
avail = len(self._buf)
while size is None or avail < size:
s = self._read()
if s == '':
break
L.append(s)
avail += len(s)
alldata = "".join(L)
if size is None:
self._buf = ''
return alldata
else:
self._buf = alldata[size:]
return alldata[:size]
def readline(self):
L = [self._buf]
self._buf = ''
while 1:
i = L[-1].find("\n")
if i >= 0:
break
s = self._read()
if s == '':
break
L.append(s)
if i == -1:
# loop exited because there is no more data
return "".join(L)
else:
alldata = "".join(L)
# XXX could do enough bookkeeping not to do a 2nd search
i = alldata.find("\n") + 1
line = alldata[:i]
self._buf = alldata[i:]
return line
def readlines(self, sizehint=0):
total = 0
inlist = []
while True:
line = self.readline()
if not line:
break
inlist.append(line)
total += len(line)
if sizehint and total >= sizehint:
break
return inlist
def fileno(self):
return self._sock.fileno()
def __iter__(self):
return self
def next(self):
line = self.readline()
if not line:
raise StopIteration
return line
class FakeSocket(SharedSocketClient):
class _closedsocket:
def __getattr__(self, name):
raise error(9, 'Bad file descriptor')
def __init__(self, sock, ssl):
sock = SharedSocket(sock)
SharedSocketClient.__init__(self, sock)
self._ssl = ssl
def close(self):
SharedSocketClient.close(self)
self._sock = self.__class__._closedsocket()
def makefile(self, mode, bufsize=None):
if mode != 'r' and mode != 'rb':
raise UnimplementedFileMode()
return SSLFile(self._shared, self._ssl, bufsize)
def send(self, stuff, flags = 0):
return self._ssl.write(stuff)
sendall = send
def recv(self, len = 1024, flags = 0):
return self._ssl.read(len)
def __getattr__(self, attr):
return getattr(self._sock, attr)

View File

@@ -1,4 +1,5 @@
# Authors: Rob Crittenden <rcritten@redhat.com>
# John Dennis <jdennis@redhat.com>
#
# Copyright (C) 2009 Red Hat
# see file 'COPYING' for use and warranty information
@@ -19,19 +20,75 @@
import httplib
import getpass
import socket
import logging
from nss.error import NSPRError
import nss.io as io
import nss.nss as nss
import nss.ssl as ssl
try:
from httplib import SSLFile
from httplib import FakeSocket
except ImportError:
from ipapython.ipasslfile import SSLFile
from ipapython.ipasslfile import FakeSocket
def auth_certificate_callback(sock, check_sig, is_server, certdb):
cert_is_valid = False
cert = sock.get_peer_certificate()
logging.debug("auth_certificate_callback: check_sig=%s is_server=%s\n%s",
check_sig, is_server, str(cert))
pin_args = sock.get_pkcs11_pin_arg()
if pin_args is None:
pin_args = ()
# Define how the cert is being used based upon the is_server flag. This may
# seem backwards, but isn't. If we're a server we're trying to validate a
# client cert. If we're a client we're trying to validate a server cert.
if is_server:
intended_usage = nss.certificateUsageSSLClient
else:
intended_usage = nss.certificateUsageSSLServer
try:
# If the cert fails validation it will raise an exception, the errno attribute
# will be set to the error code matching the reason why the validation failed
# and the strerror attribute will contain a string describing the reason.
approved_usage = cert.verify_now(certdb, check_sig, intended_usage, *pin_args)
except Exception, e:
logging.error('cert validation failed for "%s" (%s)', cert.subject, e.strerror)
cert_is_valid = False
return cert_is_valid
logging.debug("approved_usage = %s intended_usage = %s",
', '.join(nss.cert_usage_flags(approved_usage)),
', '.join(nss.cert_usage_flags(intended_usage)))
# Is the intended usage a proper subset of the approved usage
if approved_usage & intended_usage:
cert_is_valid = True
else:
cert_is_valid = False
# If this is a server, we're finished
if is_server or not cert_is_valid:
logging.debug('cert valid %s for "%s"', cert_is_valid, cert.subject)
return cert_is_valid
# Certificate is OK. Since this is the client side of an SSL
# connection, we need to verify that the name field in the cert
# matches the desired hostname. This is our defense against
# man-in-the-middle attacks.
hostname = sock.get_hostname()
try:
# If the cert fails validation it will raise an exception
cert_is_valid = cert.verify_hostname(hostname)
except Exception, e:
logging.error('failed verifying socket hostname "%s" matches cert subject "%s" (%s)',
hostname, cert.subject, e.strerror)
cert_is_valid = False
return cert_is_valid
logging.debug('cert valid %s for "%s"', cert_is_valid, cert.subject)
return cert_is_valid
def client_auth_data_callback(ca_names, chosen_nickname, password, certdb):
cert = None
@@ -55,56 +112,32 @@ def client_auth_data_callback(ca_names, chosen_nickname, password, certdb):
return False
return False
class SSLFile(SSLFile):
"""
Override the _read method so we can use the NSS recv method.
"""
def _read(self):
buf = ''
while True:
try:
buf = self._ssl.recv(self._bufsize)
except NSPRError, e:
raise e
else:
break
return buf
class NSSFakeSocket(FakeSocket):
def makefile(self, mode, bufsize=None):
if mode != 'r' and mode != 'rb':
raise httplib.UnimplementedFileMode()
return SSLFile(self._shared, self._ssl, bufsize)
def send(self, stuff, flags = 0):
return self._ssl.send(stuff)
sendall = send
class NSSConnection(httplib.HTTPConnection):
default_port = httplib.HTTPSConnection.default_port
def __init__(self, host, port=None, key_file=None, cert_file=None,
ca_file='/etc/pki/tls/certs/ca-bundle.crt', strict=None,
dbdir=None):
def __init__(self, host, port=None, strict=None, dbdir=None):
httplib.HTTPConnection.__init__(self, host, port, strict)
self.key_file = key_file
self.cert_file = cert_file
self.ca_file = ca_file
if not dbdir:
raise RuntimeError("dbdir is required")
logging.debug('%s init %s', self.__class__.__name__, host)
nss.nss_init(dbdir)
ssl.set_domestic_policy()
nss.set_password_callback(self.password_callback)
# Create the socket here so we can do things like let the caller
# override the NSS callbacks
self.sslsock = ssl.SSLSocket()
self.sslsock.set_ssl_option(ssl.SSL_SECURITY, True)
self.sslsock.set_ssl_option(ssl.SSL_HANDSHAKE_AS_CLIENT, True)
self.sslsock.set_handshake_callback(self.handshake_callback)
self.sock = ssl.SSLSocket()
self.sock.set_ssl_option(ssl.SSL_SECURITY, True)
self.sock.set_ssl_option(ssl.SSL_HANDSHAKE_AS_CLIENT, True)
# Provide a callback which notifies us when the SSL handshake is complete
self.sock.set_handshake_callback(self.handshake_callback)
# Provide a callback to verify the servers certificate
self.sock.set_auth_certificate_callback(auth_certificate_callback,
nss.get_default_certdb())
def password_callback(self, slot, retry, password):
if not retry and password: return password
@@ -114,43 +147,102 @@ class NSSConnection(httplib.HTTPConnection):
"""
Verify callback. If we get here then the certificate is ok.
"""
if self.debuglevel > 0:
print "handshake complete, peer = %s" % (sock.get_peer_name())
logging.debug("handshake complete, peer = %s", sock.get_peer_name())
pass
def connect(self):
self.sslsock.set_hostname(self.host)
logging.debug("connect: host=%s port=%s", self.host, self.port)
self.sock.set_hostname(self.host)
net_addr = io.NetworkAddress(self.host, self.port)
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.sslsock.connect(net_addr)
self.sock = NSSFakeSocket(sock, self.sslsock)
logging.debug("connect: %s", net_addr)
self.sock.connect(net_addr)
class NSSHTTPS(httplib.HTTP):
# We would like to use HTTP 1.1 not the older HTTP 1.0 but xmlrpclib
# and httplib do not play well together. httplib when the protocol
# is 1.1 will add a host header in the request. But xmlrpclib
# always adds a host header irregardless of the HTTP protocol
# version. That means the request ends up with 2 host headers,
# but Apache freaks out if it sees 2 host headers, a known Apache
# issue. httplib has a mechanism to skip adding the host header
# (i.e. skip_host in HTTPConnection.putrequest()) but xmlrpclib
# doesn't use it. Oh well, back to 1.0 :-(
#
#_http_vsn = 11
#_http_vsn_str = 'HTTP/1.1'
_connection_class = NSSConnection
def __init__(self, host='', port=None, key_file=None, cert_file=None,
ca_file='/etc/pki/tls/certs/ca-bundle.crt', strict=None):
def __init__(self, host='', port=None, strict=None, dbdir=None):
# provide a default host, pass the X509 cert info
# urf. compensate for bad input.
if port == 0:
port = None
self._setup(self._connection_class(host, port, key_file,
cert_file, ca_file, strict))
# we never actually use these for anything, but we keep them
# here for compatibility with post-1.5.2 CVS.
self.key_file = key_file
self.cert_file = cert_file
self.ca_file = ca_file
self._setup(self._connection_class(host, port, strict, dbdir=dbdir))
class NSPRConnection(httplib.HTTPConnection):
default_port = httplib.HTTPConnection.default_port
def __init__(self, host, port=None, strict=None):
httplib.HTTPConnection.__init__(self, host, port, strict)
logging.debug('%s init %s', self.__class__.__name__, host)
nss.nss_init_nodb()
self.sock = io.Socket()
def connect(self):
logging.debug("connect: host=%s port=%s", self.host, self.port)
net_addr = io.NetworkAddress(self.host, self.port)
logging.debug("connect: %s", net_addr)
self.sock.connect(net_addr)
class NSPRHTTP(httplib.HTTP):
_http_vsn = 11
_http_vsn_str = 'HTTP/1.1'
_connection_class = NSPRConnection
#------------------------------------------------------------------------------
if __name__ == "__main__":
h = NSSConnection("www.verisign.com", 443, dbdir="/etc/pki/nssdb")
h.set_debuglevel(1)
h.request("GET", "/")
res = h.getresponse()
print res.status
data = res.read()
print data
h.close()
logging.basicConfig(level=logging.DEBUG,
format='%(asctime)s %(levelname)-8s %(message)s',
datefmt='%m-%d %H:%M',
filename='nsslib.log',
filemode='a')
# Create a seperate logger for the console
console_logger = logging.StreamHandler()
console_logger.setLevel(logging.DEBUG)
# set a format which is simpler for console use
formatter = logging.Formatter('%(levelname)s %(message)s')
console_logger.setFormatter(formatter)
# add the handler to the root logger
logging.getLogger('').addHandler(console_logger)
logging.info("Start")
if False:
conn = NSSConnection("www.verisign.com", 443, dbdir="/etc/pki/nssdb")
conn.set_debuglevel(1)
conn.connect()
conn.request("GET", "/")
response = conn.getresponse()
print response.status
#print response.msg
print response.getheaders()
data = response.read()
#print data
conn.close()
if True:
h = NSSHTTPS("www.verisign.com", 443, dbdir="/etc/pki/nssdb")
h.connect()
h.putrequest('GET', '/')
h.endheaders()
http_status, http_reason, headers = h.getreply()
print "status = %s %s" % (http_status, http_reason)
print "headers:\n%s" % headers
f = h.getfile()
data = f.read() # Get the raw HTML
f.close()
#print data

View File

@@ -125,30 +125,6 @@ def import_pkcs12(input_file, input_passwd, cert_database,
"-k", cert_passwd,
"-w", input_passwd])
def client_auth_data_callback(ca_names, chosen_nickname, password, certdb):
cert = None
if chosen_nickname:
try:
cert = nss.find_cert_from_nickname(chosen_nickname, password)
priv_key = nss.find_key_by_any_cert(cert, password)
return cert, priv_key
except NSPRError, e:
logging.debug("client auth callback failed %s" % str(e))
return False
else:
nicknames = nss.get_cert_nicknames(certdb, nss.SEC_CERT_NICKNAMES_USER)
for nickname in nicknames:
try:
cert = nss.find_cert_from_nickname(nickname, password)
if cert.check_valid_times():
if cert.has_signer_in_ca_names(ca_names):
priv_key = nss.find_key_by_any_cert(cert, password)
return cert, priv_key
except NSPRError, e:
logging.debug("client auth callback failed %s" % str(e))
return False
return False
def get_value(s):
"""
Parse out a name/value pair from a Javascript variable.

View File

@@ -60,30 +60,6 @@ def ipa_self_signed():
else:
return False
def client_auth_data_callback(ca_names, chosen_nickname, password, certdb):
cert = None
if chosen_nickname:
try:
cert = nss.find_cert_from_nickname(chosen_nickname, password)
priv_key = nss.find_key_by_any_cert(cert, password)
return cert, priv_key
except NSPRError, e:
logging.debug("client auth callback failed %s" % str(e))
return False
else:
nicknames = nss.get_cert_nicknames(certdb, nss.SEC_CERT_NICKNAMES_USER)
for nickname in nicknames:
try:
cert = nss.find_cert_from_nickname(nickname, password)
if cert.check_valid_times():
if cert.has_signer_in_ca_names(ca_names):
priv_key = nss.find_key_by_any_cert(cert, password)
return cert, priv_key
except NSPRError, e:
logging.debug("client auth callback failed %s" % str(e))
return False
return False
def find_cert_from_txt(cert, start=0):
"""
Given a cert blob (str) which may or may not contian leading and