http://lnwdl.blog.163.com/blog/static/38830412201471565029116/
SSL session
SSL session与SSL connection是不同的概念。SSL session指的是通过握手而产生的一些参数和加密秘钥的集合;然而SSL connection是指利用某个session建立起来的活动的会话。换句话来说,connection是会话的进程,而session是建立这个会话所需要的一些参数。知道了这一点,我们就来看一下OpensSSL中提供的关于SSL session的一些函数。
对于SSL的服务器来说,在与客户端建立连接时的消耗远远大于维持连接的消耗,那么使用session缓存是减少服务器负担的有效方式。OpenSSL使用SSL_SESSION结构实现session。虽然实现session缓存主要在服务器端,但是为了高效的实现方式,在客户端也需要维持之前的session信息。
对于服务器端来说,一旦建立起来一个连接,仅仅需要标注一下数据和缓存一下session。标注的标签,我们称之为session ID上下文。建立起一个session时候,服务器给他打上一个超时时间,超时之后,这个session就会被丢弃。默认的情况下服务器自动地清理过期的session。对于客户端来说,它从服务端获得SSL_SESSION结构。客户端可以将它存储起来,当再次建立另一个connection的时候使用它。例如,浏览器可能需要建立多个连接来显示SSL服务器提供的信息。有了session之后,客户端可以迅速建立连接,服务端也由于不用再协商参数,大大减少了负担。
如上所述,由服务端决定有效的session ID上下文。当一个客户端要向一个缓存session的服务端建立连接的时候,只需要提供一次建立session所需要的信息。如果信息正确,就会产生常的握手过程,然后一个新的session建立起来。如果客户端保存了刚刚产生的session,那么它就有了为下次再进行建立connection的session ID上下文。
客户端的SSL session
当connection建立起来后,客户端通过函数SSL_get_session返回SSL_SESSION结构,这个结构包含在建立SSL connection时所需要的参数。如果函数返回NULL,说明在建立connection后并没有建立session。实际上,我们通常调用SSL_get0_session或者SSL_get1_session。这两个变种用于保证正确维持SSL_SESSION结构中的引用计数。SSL_get0_session不会改变引用计数,而SSL_get1_session会使计数增加。通常,由于SSL_SESSION机构会异步地超时,我们倾向于使用后面的函数。如果超时了,我们就会有一个指向无效数据的结构。使用了SSL_get1_session的话,当我们用完这个session时必须要使用SSL_SESSION_free函数,防止内存泄露。
在存贮了SSL_SESSION结构的引用之后,我们就可以关闭SSL connection和底层的通道(通常为socket)了。对于大多数的客户端来说,在同一时间不会有很多的SSL session,所以将他们缓存在内存中就足够了。如果不想这样,我们可以使用函数PEM_write_bio_SSL_SESSION或者PEM_write_SSL_SESSION函数将session写到硬盘中,再次使用的时候调用PEM_read_bio_SSL_SESSION或者PEM_read_SSL_SESSION函数。
在使用一个保存的session时候,需要在调用SSL_connect之前调用SSL_set_session函数。SSL_SESSION结构的引用计数会自动地增加,所以之后需要调用SSL_SESSION_free函数。在重用一个session时,最好在断开之前调用SSL_get1_session函数来更新缓存的SSL_SESSION结构,因为在连接维持的过程中可能会发生重新协商。重新协商会产生一个新的SSL_SESSION结构,所以我们要时刻保持最新的。
现在我们了解了应用session缓存的基本原则,下面我们将它应用于具体的客户端应用。下面的伪代码展示了客户端的session缓存。
ssl = SSL_new(ctx)
... setup underlying communications layer for ssl ...
... connect to host:port ...
if (saved session for host:port in cache)
SSL_set_session(ssl, saved session)
SSL_SESSION_free(saved session)
SSL_connect(ssl)
call post_connection_check(ssl, host) and check return value
... normal application code here ...
saved session = SSL_get1_session(ssl)
if (saved session != NULL)
enter saved session into cache under host:port
SSL_shutdown(ssl)
SSL_free(ssl)
服务端的SSL session
所有的session必须都要有session ID上下文。对于服务端来说,session缓存默认是不使能的,可以通过调用SSL_CTX_set_session_id_context函数来进行使能。产生session ID上下文的目的是保证重用的session的使用目的与session创建时的使用目的是一致的。比如,在SSL web服务器中产生的session不能自动地在SSL FTP服务中使用。于此同时,我们可以使用session ID上下文来实现对我们的应用的更加细粒度的控制。比如,认证后的客户端应该与没有进行认证的客户端有着不同的session ID上下文。上下文的内容我们可以任意选择。正是通过函数SSL_CTX_set_session_id_context函数来设置上下文的,上下文的数据时第二个参数,第三个参数是数据的长度。
在设置了session ID上下文后,服务端就开启了session缓存;但是我们的配置还没有完成。Session有一个限定的生存期。在OpenSSL中的默认值是300秒。如果我们需要改变这个生存期,使用函数SSL_CTX_set_timeout。尽管服务端默认地会自动地清除过期的session,我们仍然可以手动地调用SSL_CTX_flush_sessions来进行清理。比如,当我们关闭自动清理过期session的时候,就需要手动进行了。
一个很重要的函数:SSL_CTX_set_session_cache_mode,它允许我们改变对相关缓存的行为。与OpenSSL中其它的模式设置函数一样,模式使用一些标志的逻辑或来进行设置。其中一个标志是SSL_SESS_CACHE_NO_AUTO_CLEAR,它关闭自动清理过期session的功能。这样有利于服务端更加高效严谨地进行处理,因为默认的行为可能会有意想不到的延迟;所以,关闭它并且空闲的时候手动地调用清理函数能够更加地高效。另一个标志是SSL_SESS_CACHE_NO_INTERNAL_LOOKUP。到目前为止,我们仅仅使用内部查找机制,但是后面会介绍到,我们可以不使用内部的查找机制。
Session缓存和连接重新协商同时使用会有一些微妙。在应用缓存的服务端,我们应该意识到潜在的隐患。后面我们会详细讨论。
磁盘缓存的架构
OpenSSL中session缓存机制的API中用于设置外部缓存时包含了三个回调函数。像其它的OpenSSL的回调函数一样,三个函数用于设置回调函数的函数指针。每个函数中,第一个参数是SSL_CTX结构,第二个参数是指向回调函数的函数指针。
-
通过SSL_CTX_sess_set_new_cb函数设置的回调函数会在SSL_CTX结构创建新的SSL_SESSION结构时候被调用。这个回调函数让我们能够将新产生的session放到我们自己的容器中。如果回调函数返回0,那么session将不会被缓存;否则,session将会被缓存。
Function protype:int new_session_cb(SSL *ctx, SSL_SESSION *session);
@ctx: SSL连接的结构体
@session: 新产生的session结构
-
通过函数SSL_CTX_sess_set_remove_cb设置的回调函数会在一个SSL_SESSION结构销毁的时候调用。这个回调函数将在session结构由于无效或者过期而销毁之前被调用。
Function protype:void remove_session_cb(SSL *ctx, SSL_SESSION *session);
@ctx: SSL连接的结构体
@session: 由于无效或者过期而即将被销毁的session结构
-
通过SSL_CTX_set_get_cb函数来设置缓存检索的回调函数。这个回调函数会在内部缓存中检索不到请求中的session时被调用。也就是说,这个回调函数应该在我们的外部缓存中进行查找以期望找到匹配。
Function protype:SSL_SESSION *get_session_cb(SSL *ctx, unsigned char *id, int len, int *ref);
@ctx: SSL连接的结构体
@id: 对方请求中的session ID。要将session ID和session ID上下文区分开。上下文是在特定的应用程序中指定的,针对一组session,而session ID是对端的标志。
@len: session ID的长度。由于session ID可以为任意的字符数值,不一定会以NULL结尾,所以,需要同时指定它的长度。
@ref: 回调函数的输出。用于允许回调函数指定返回的session结构的引用计数是否应该增加。如果返回非0,那么引用计数应该增加;否则,将返回0。
下面的伪代码是上述特性的一个简单实现。我们使用文件来缓存session,直接用文件系统内置的锁机制。我们可以使用PEM_write_bio_SSL_SESSION来讲文件写到磁盘,但是不允许加密。记住,SSL_SESSION结构保存协商后的公钥;所以,里面的内容在排列的时候需要进行保护。我们也可以调用底层的PEM_ASN1_write_bio函数。对于某些应用程序,需要一个安全的目录去写就已经足够了。通常,更安全的方式是使用内存锁进行加密。
new_session_cb()
{
acquire a lock for the file named according to the session id;
open file named according to sesson id;
encrypt and write the SSL_SESSION object to the file;
release the lock;
}
remove_session_cb()
{
acquire a lock for the file named according to the session id;
remove the file named according to the session id;
release the lock;
}
get_session_cb()
{
acquire a lock for the file named according to the session id in the 2nd arg;
read and decrypt contents of file and create a new SSL_SESSION object;
release the lock;
set the integer referenced by the fourth parameter to 0;
return the new session object;
}
上面的实现的框架,提供了一个强大的session缓存机制。使用了文件系统,缓存将不再受到内存的限制。另外,使用内存进行缓存更加简单一些,以为不会像外村那样会有其他的应用访问这段数据。
SSL重新协商
SSL重新协商本质上就是在一个连接中进行SSL握手。会导致客户端的凭据进行重新评估,并且新建一个session。
由于重新协商会产生一个新的session,session的秘钥会被替换。对于长时间维持的SSL连接和大量传输数据的连接来说,定时的更改session的秘钥会更好一些。通常来说,session秘钥存在的时间越长,带来的风险越大。使用重新协商,我们可以替换session的秘钥,不会导致使用一个session秘钥加密过多的数据。
SSL重新协商可以在应用数据传输的过程中发生。当心的握手完成后,双方都使用新的秘钥。在SSL连接上请求重新协商的函数为SSL_renegotiate。
这个函数并不会在调用之后马上执行新的握手;而是,它设置一个标志,产生一个重新协商的请求并发给对方。这个请求会在这个SSL连接的下一个I/O发生的时候发送出去。如果对方不相应这个请求而是继续传送数据,就不会发生重新协商。只有请求者检查通过之后才会发生重新协商。这对于应用程序连接其他非OpenSSL实现的SSL的应用来说非常重要。
也可以进行显示的重新协商。也就是说,我们发送了一个重新协商请求,在新的握手完成之前不再发送和接收应用数据。有时在一个长时间的连接上面刷新session秘钥,新的秘钥应用于其他的应用目的(即改变了session ID的上下文)。它允许我们在服务端更新客户端的凭证。比如,在重新协商的时候,我们的服务器要求客户端提供一个有效的证书。这样在刚开始的连接中就不用过分限制客户端的意图了。
举例来说,一个简单的协议通过SSL通信向服务端发送命令。所有命令中的一个子集的命令只能又管理员来执行。我们的目标是允许所有人都能够连接到服务端,普通的用户能够输入通常的命令,只有管理员能够执行那些保留的命令。如果我们要求所有用户在建立连接时都提供相应的用户证书,我们必须为每个可能的用户特别是管理员都颁发相应的证书,这样做虽然高效但是很麻烦。或者,我们建立两个服务,一个为普通用户,一个为管理员。这也不是最优的方式,因为我们消耗了更多的资源。
我们可以使用重新协商来解决上述问题。首先,我们允许所有的用户不使用证书就连接上我们的服务器。当一个用户连接后,它发送它自己的命令。如果涉及到那些保留的命令,我们要求用户提供更多的条件以进行重新协商。如果用户重新协商成功,我们接受这些命令,否则不允许这些命令。这样,我们只需要为管理员颁发一个证书即可。这种方式显然比前面的两种方式更好一些,因为它使得用户在获得更严谨地权限之前就可以进行连接。
如前所述,执行一个被动的重新协商(比如,在应用数据I/O期间进行握手),我们只要调用函数SSL_renegotiate。通常,应用程序会在应用I/O上停止下来而进行握手,但是确保握手真正执行时非常重要的。不幸的是,在OpenSSL 0.9.6版本之前这个很难做到,事实上,在这个版本中,很多重新协商的特性都不是很明确。然而,后来的0.9.7版本承诺解决这些问题。
前面的讨论中我们的重新协商局限在了服务端。通常,应用大多是这样的,因为服务端保存session的缓存。虽然不常用,但是客户端也可以发出重新协商请求。
仍然不是很清晰的一点是我们如何响应重新协商的请求。幸运的是这些都由OpenSSL库为我们实现了。