我已经阅读了一些相关问题,但它们都没有真正帮助我。那么,让我给你一个背景故事: 我正在写一个投票系统,你有几个服务器来管理注册,交换投票卡和投票。在注册用户提交他的个人数据期间,服务器检查数据是否与数据库中的数据匹配,然后接受用户的证书并将其添加到其trustStore,如下所示:
KeyStore.Entry entry = new KeyStore.TrustedCertificateEntry(cert);
trustKeyStore.setEntry(alias, entry, null);
然后将trustStore存储在文件中。在注册期间,许多用户注册他们的证书,然后他们连接到请求客户端和服务器身份验证的不同服务器,这里出现问题。我认为这个“其他服务器”可以使用前面提到的trustStore来与用户一起执行SSLHandshake,但看起来trustStore“只记住”最后注册的用户。我正在创建SSLServerSocket:
tmf.init(trustKeyStore);
kmf.init(keyStore, password);
// create, init SSLContext
SSLContext sslCtx = SSLContext.getInstance("TLS");
sslCtx.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
// create SSLSocketFactory and SSLSocket
SSLServerSocketFactory sslSocketFactory = (SSLServerSocketFactory)sslCtx.getServerSocketFactory();
sslSocket = (SSLServerSocket)sslSocketFactory.createServerSocket(1234);
正如我之前提到的,双方身份验证工作正常,但仅适用于trustStore中的最后一个条目。所以,现在最后我的问题是 - 我是否必须为每个用户创建一个单独的trustStore?我必须重载TrustManager吗?基本上,有没有什么方法可以让SSLContext / SSLEngine迭代我所有的可信证书,如果找到匹配的那个,就进行握手?
更新
我想我需要澄清一些事情。首先,这不是一个Web应用程序,只是一个普通的客户端 - 服务器Java swing应用程序。使用的每个证书(服务器或客户端)都是自签名的,但服务器的证书是构建到客户端应用程序中的,因此当客户端连接到服务器时,他可以比较证书(工作)。我也试图像Bruno建议的那样解决它 - 在用户注册后,服务器检查他的数据是否有效,如果是,它为客户端发出新证书,将其添加到他的信任库并将其发送到客户端。但即使对于最后一位注册的客户,这也不起作用。
答案 0 :(得分:3)
(在更改问题后编辑。)
方法1:
解决此问题的一种方法是使用CA,可能是您自己的CA.
方法2:
在握手级别接受任何客户端证书。虽然从客户端角度接受任何服务器证书是一个坏主意,因为它引入了MITM攻击的可能性(例如,请参阅here和here),在握手期间接受任何客户端证书并不会出现同样的问题。
在握手结束时,如果仍然验证了服务器证书,您将知道:
CertificateVerify
消息保证,该消息使用客户端的私钥来签署到目前为止在握手期间交换的所有消息的串联,包括服务器证书和客户端证书。这实际上并不依赖于客户端证书本身的(信任)验证,只有在客户端证书中的公钥可以验证TLS CertificateVerify
消息中的签名时才会起作用。通过这种方式,您将知道服务器获取的客户端证书由具有其匹配私钥的人使用。 您不知道的是,公钥证书所属的是谁,即另一端的用户是谁,因为您错过了通常使用CA执行的验证步骤,在颁发证书之前,它本身应该依赖于带外机制。由于他们使用的是自签名证书,因此您无论如何都必须执行此带外步骤,可能是在注册过程中通过某些信息。
这样做可以让您更轻松地管理用户及其使用证书的方式。您只需在服务器常用的数据库中存储和/或查找当前用户的证书即可。您可以通过查看应用程序中的SSLSession
来访问证书。您将在getPeerCertificates()
返回的数组的位置0中找到用户证书。然后,您可以简单地将您看到的凭证(再次,因为双向握手成功)与您之前看到的凭证进行比较,以便在您的应用程序中进行身份验证(和授权)。
请注意,即使您刚接受了自己添加的自签名证书,除了主题DN之外,您仍然必须在系统中跟踪此公钥信息,因为您无法控制主题DN(两个用户可以选择有意或无意使用)。
要实现此目的,您需要使用SSLContext
配置服务器,该checkClientTrusted
使用X509TrustManager
并不会在其SSLContext
方法中抛出任何异常。 (如果您出于其他目的使用相同的TrustManager
,我会获得默认的PKIX checkServerTrusted
并将调用委托给getAcceptedIssuers()
。)因为您已经可能不会知道这些自签名证书的主题/发行者DN是什么,您应该通过在<keygen />
中返回一个空数组来发送一个接受的CA(*)的空列表(参见示例{{ 3}})。
(*)TLS 1.0对此主题保持沉默,但TLS 1.1明确允许空列表(具有未指定的行为)。在实践中,它适用于大多数浏览器,即使使用SSLv3 / TLSv1.0:在这种情况下,浏览器将显示可供选择的完整证书列表(或者选择它找到的第一个证书并配置为选择一个自动)。
(我在这里离开了什么更具体的HTTP ...现在有点脱离背景,但这可能是其他人感兴趣的。)
方法1:
您可以将颁发的证书集成为初始注册的一部分。当用户注册他们的详细信息时,他们还会申请一个证书,该证书会立即由您的注册服务器发布到他们的浏览器中。 (如果您对此不熟悉,可以使用javax.servlet.request.X509Certificate
,here(在Firefox上)或IE上的ActiveX控件在浏览器中发布证书,这取决于Windows的版本。)
方法2:
在Servlet环境中,您可以在servlet请求{{1}}属性
中获取它此外,这往往会改善用户体验,因为拒绝客户端证书并不一定会终止连接。你仍然可以提供一个网页(可能有一个HTTP 401状态,虽然在技术上,它需要伴随一个挑战机制,并且没有一个用于证书),至少告诉你的用户出了什么问题。握手失败(握手中的客户端证书验证会在出现问题时导致)对于不了解证书的用户来说可能会非常混乱。缺点是它很难退出&#34; (与HTTP基本身份验证类似),因为大多数浏览器都缺乏用户界面支持。
对于实现,如果您正在使用Apache Tomcat,如果您正在使用Apache Tomcat 6.0.33或更高版本,则可能对CRMF或this SSLImplementation
感兴趣。
答案 1 :(得分:3)
我已经解决了这个问题。正如经常发生的那样,我身边出现了一个愚蠢的错误 - 其中一个服务器覆盖了整个密钥库。我花了很多时间来确定如何建立这种ssl通信,而且我在网上找不到太多关于它的信息,所以我希望这将有助于将来的某些人。
要设置仅限服务器的通信,您需要执行以下操作: 1.在客户端获取(不知何故,在我的情况下,它是在客户端应用程序中构建)服务器的证书,并将其添加到您的信任库:
KeyStore clientTrustedKeyStore = KeyStore.getInstance(KeyStore.getDefaultType());
clientTrustedKeyStore.load(null, "password".toCharArray());
KeyStore.Entry entry = new KeyStore.TrustedCertificateEntry(cert);
clientTrustedKeyStore.setEntry("alias", entry, null);
在客户端或服务器端创建sslsockets时,需要使用keyStore(服务器)或trustStore(客户端)将SSLContext“提供”到SSLContext:
tmf = TrustManagerFactory.getInstance("SunX509");
kmf = KeyManagerFactory.getInstance("SunX509");
tmf.init(trustKeyStore);
kmf.init(keyStore, password);
// create, init SSLContext
SSLContext sslCtx = SSLContext.getInstance("TLS");
sslCtx.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
然后创建sslsocketfactory和套接字。
创建套接字后(仅在服务器端有用)设置身份验证模式:
sslSocket.setNeedClientAuth(布尔值);
如果设置双方验证,您必须更改的内容是:验证模式(显然),将证书添加到客户端和服务器端的trustStores。
双方验证的另一个问题是以下方案不起作用(尽管,至少对我来说,这似乎是合乎逻辑的): 1.服务器发布自己的自签名证书,将其添加到其trustStore,然后将其发送到客户端,以便在将来的连接中进行身份验证。 为什么它不起作用?因为当客户端获得证书时,他只能将其添加到他的密钥库中:
ks.setCertificateEntry(alias,cert);
当涉及到SSLHandshake时,它不会做什么,您必须使用setEntry添加到密钥库的证书进行身份验证:
ks.setKeyEntry(alias,keyPair.getPrivate(),keyStorePassword,certChain);
其中certChain就是那样。
Certificate[] certChain = {myCert};
这就是全部,我确信有些人这一切都很明显,但我希望它能帮助像我这样的SSL初学者:)