nginx实现keyless解决方案
2021-01-23 20:22:05 Author: www.freebuf.com(查看原文) 阅读量:81 收藏

简介

当企业把业务迁移到云WAF/CDN边缘节点上,需向云厂商提供业务的私钥安全性不能得到保证,且若业务私钥证书发生变化或频繁修改需要受限于人。风险:一旦服务端的私钥泄露会导致恶意攻击者伪造虚假的服务器和客户端通信,通信内容也存在被劫持和解密的风险。keyless源于clouldflare,采用keyless方案私钥部署在客户自己的服务器,无需向把业务私钥部署在云/CDN边缘节点上。

1611405699_600c1983244e1a36d4bc0.png!small?1611405694125

1611405932_600c1a6cc342367592973.png!small?1611405927816

clouldflare keyless项目地址:https://blog.cloudflare.com/keyless-ssl-the-nitty-gritty-technical-details/

cloudflare keyless开源项目地址:https://github.com/cloudflare/keyless

相关基础知识介绍:

加解密套件知识普及

MAC(Message authentication code):消息认证码

PRF(pseudorandom function):伪随机函数

SHA (Secure Hash Algorithm):安全散列算法

  • 对称密码:

DES:是以64比特的明文为一个单位来进行加密的,密钥长度是64比特

三重DES: 就是将DES重复3次,有3个密钥

AES(Advanced Encryption Standard):是一种新标准的对称密码算法,已取代DES

分组长度128比特,密钥长度128、192、256三种规格

  • 分组密码模式 :

ECB(Electronic CodeBook):将明文分组加密之后的结果将直接成为密文分组

CBC(Cipher Block Chaining):密文分组链接的模式

CFB(Cipher FeedBack):密文反馈模式

OFB(Output-Feedback):输入反馈模式

CTR(CounTeR):计数器模式

GCM(Galois Counter Mode):Galois/计数器模式

  • 填充模式:对当明文长度不为分组长度的整数倍时,需要在最后一个分组中填充一些数据使其填满一个分组长度,攻击者会利用这个利用这个在最后一个分组填充一些数据。
  • 单向散列函数

输入的是消息输出的是散列值,任意长度的消息计算出固定长度的散列值,消息不同散列值也不同

应用:MD4、MD5;SHA-2系列(SHA-256,SHA-384,SHA-512,数字表示计算后的散列值长度)

  • 密钥交换算法

RSA本质上是为了解决密钥配送的问题,密钥配送的是配送的是运算对称密钥的关键信息,并不是对称密钥

RSA:这是一个标准的密钥交换算法,在ClientKeyExchange阶段客户端生成预主秘钥,不支持向前保密,并以服务器公钥加密传送给服务器

DHE_RSA:临时Diffie-Hellman(ephemeral Diffie-Hellman, DHE),支持向前保密,缺点是执行缓慢,DHE是一种秘钥协定算法,进行协商的团体都对密钥生成产生作用,并对公共密钥产生作用

ECDHE_RSA和ECDHE_ECDSA:

临时椭圆曲线Diffe-Hellman(ephemeral elliptic curve Diffie-Hellman, ECDHE)密钥交换建立在椭圆曲线加密的基础之上。执行很快而且提供了向前保密,和DHE类似

过滤了一台设备上一天的数据

加密套件

完整名称

条数

比例

ECDHE-RSA-AES128-SHA256

TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256

590549

89.67%

DHE-RSA-AES256-GCM-SHA384

TLS_DHE_RSA_WITH_AES_256_GCM_SHA384

33802

5.13%

ECDHE-RSA-AES256-GCM-SHA384

TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384

18150

2.76%

DHE-RSA-AES256-SHA256

TLS_DHE_RSA_WITH_AES_256_CBC_SHA256

12845

1.95%

ECDHE-RSA-AES256-SHA

TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA

1462

0.22%

ECDHE-RSA-CHACHA20-POLY1305

TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256

1388

0.21%

AES256-SHA256

TLS_RSA_WITH_AES_256_CBC_SHA256

302

0.05%

ECDHE-RSA-AES128-GCM-SHA256

TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256

36

0.01%

DHE-RSA-AES256-SHA

TLS_DHE_RSA_WITH_AES_256_CBC_SHA

23

0%

AES256-SHA

TLS_RSA_WITH_AES_256_CBC_SHA

1

0%

TLS握手中使用的密码技术

TLS记录协议位于TLS协议的下层,是负责使用对称密码对消息进行加密通信(对消息压缩、加密以及数据的认证)的部分

  • TLS握手协议中使用的密码技术

公钥密码:加密预主秘钥用的

单向散列函数:构成伪随机数生成器

数字签名:验证证书用的(单向散列函数计算公钥密码的散列值,加密后得到)

伪随机书生成器:生成预主秘钥

                      生成初始化向量(可以使用对称密码,单向散列函数来构建)?

                     根据主密钥生成密钥(密码参数)?

  • TLS记录协议中使用的密码技术

对称密码(CBC模式):确保片段的机密性

消息认证码:确保片段的完整性并进行认证(单向散列函数和密钥组合而成,也可以通过对称密码生成,应用单向散列函数计算密钥+消息构成的)

认证加密(AEAD,Authenticated-Encryption with Associated-Data用于关联数据的认证加密):确保片段的完整性和机密性并进行认证?

HTTPS中所用到的密码技术

证书:公钥、数字签名和指纹组合而成,一般讲是基于指纹的数字签名,一堆的东西就认证公钥,为了保证不可否认行、认证、完整性

Keyless原理

总体架构

1611396119_600bf417e29ca5ac46d20.png!small?1611396115042

密钥交换算法类

握手协议

密钥建立

认证

RSA

RSA

RSA

DH

DH

RSA/DSA

client random 是第1个随机数R1(公开),对应wireshark抓包里“Client Hello”的Random

server random 是第2个随机数R2(公开),对应wireshark抓包里“Server Hello”的Random

premaster 是第3个随机数R3(私密),该随机数是由客户端创建,然后客户端用服务端传来的证书对premaster secret进行加密,生成premaster secret用来实际传输,对应抓包里的“Client Key Exchange”

服务端用私钥对premaster secret解密,得到premaster,这样只有客户端和服务端知道premaster

最终,客户端和服务端用公开的随机数R1、R2、双方私密的premaster(R3)组合起来,通过预定的算法生成一个hash值,作为之后的对话密钥(session key)

1611396175_600bf44f68a9fba8c550d.png!small?1611396170646

RSA密钥交换算法主密钥计算

Client Random和Server Random明文传输,中间人可以直接查看,客户端生成于中Premaster Secret后,如果有证书私钥就可以直接通过这三个参数解得主密钥

1611396224_600bf480419a9c2d9e060.png!small?1611396219433

标准RSAkeyless握手方案

工作在:Server端的ChangeCipherSpec阶段

基于DH的完整握手主密钥的计算

从密钥交换流程来说,DH算法和ECDHE一样,二者的主要区别见该页备注里的注意点1~3

client random 是第1个随机数R1(公开),对应wireshark抓包里“Client Hello”的Random ②a、server random 是第2个随机数R2(公开),对应wireshark抓包里“Server Hello”的Random

服务端自己创建一个随机数或者 直接从证书中拿公钥信息(图例是拿公钥信息),记为R3 ,结合上面的两个公开的随机数,通过DH算法算出来服务端DH参数=(R1 * R2 * R3) ,对应wireshark抓包里“Server Key Exchange”的Pubkey。

服务端用私钥,对两个公开随机数R1、R2和服务端的DH参数进行签名,对应wireshark抓包里“Server Key Exchange”的Signature

客户端用证书公钥验证Signature,验证服务端确实拥有私钥后,客户端就创建一个随机数,记为R4,通过DH算法算出来客户端DH参数=(R1 * R2 * R4) ,对应wireshark抓包里“Client Key Exchange”的Pubkey 。 这样,客户端和服务端用对方发来的DH参数,结合各自的私有随机数R3或R4,分别计算得到相同的premaster = (R1 * R2 * R3 * R4) ,且只有客户端和服务端知道premaster 最终,客户端和服务端用公开的随机数1、随机数2、双方私密的premaster组合起来,通过预定的算法生成一个hash值,作为之后的对话密钥(session key)

1611396275_600bf4b3cdf317cfc60b1.png!small?1611396271863

Server DH Parameter 是用证书私钥签名的,客户端使用证书公钥就可以验证服务端合法性,相比 RSA 密钥交换,DH 由传递 Premaster Scret 变成了传递 DH 算法所需的 Parameter,然后双方各自算出 Premaster Secret。由于 Premaster Secret 无需交换,中间人就算有私钥也无法获得 Premaster Secret 和 Master Secret。

ServerKeyExchange

基于DH的keyless的完整握手

1611396355_600bf5033a90ed460ea1d.png!small?1611396351125

工作在:Server端的ServerKeyExchange阶段

开源项目做了什么

1611396458_600bf56a473cae682339b.png!small?1611396453411

keyless server安装和配置

存储给定的私钥。

使用加速卡(EXAR)进行解密,签名操作。

状态信息统计。

开源项目地址:https://github.com/cloudflare/keyless

需要进行二次开发,开源版本很多细节处理的不好。

On Centos:

sudo yum install gcc automake libtool
 sudo yum install rpm-build rubybgems ruby-devel # only required for packages
 sudo gem install fpm --no-ri --no-rdoc # only required for packages

安装 make即可,make test测试

参数说明 --port keyless server端的监听端口

          --ip keyless server端监听的ip

          --server-cert和--server-key签发的证书

         --private-key-directory 用户证书对应的私钥存放文件夹

          --ca-file生成的根证书

          --pid-file pid文件

         --num-workers 线程数

        --verbose打印日志

        --daemon守护进程开启

nginx于keyless server交互

1611398197_600bfc3567a5d73fd67ff.png!small

在SSL_do_handshake解密和签名处理过程中增加一个keyless状态。

PREPARE REQUEST状态,封装keyless请求报文,然后将状态设置为SEND REQUEST,SSL_do_handshake函数返回,nginx将keyless_connection的wev放到epoll里;

SEND REQUEST状态发送keyless请求,成功后将状态设置为RECEIVE RESPONSE,SSL_do_handshake函数返回,nginx将keyless_connection的rev放到epoll里;

RECEIVE RESPONSE状态读请求,全部读完将状态设置为FINISH;如果未读完数据SSL_do_handshake函数返回,nginx将keyless_connection的rev放到epoll里;

FINISH 继续由openssl原有的逻辑处理。

如果rev和wev超时,则关闭ssl_connection。

nginx 处理https握手

ngx_http_init_connection中recv→handler设置为ngx_http_ssl_handshake,把这个读时间加入到epoll中,重点看handshake这个函数

static void

ngx_http_ssl_handshake(ngx_event_t *rev)

{

...

n = recv(c->fd, (char *) buf, size, MSG_PEEK);

//判断协议

if (n == 1) {

if (buf[0] & 0x80 /* SSLv2 */ || buf[0] == 0x16 /* SSLv3/TLSv1 */) {

// 获取loc conf和server conf

clcf = ngx_http_get_module_loc_conf(hc->conf_ctx,

ngx_http_core_module);

if (clcf->tcp_nodelay && ngx_tcp_nodelay(c) != NGX_OK) {

ngx_http_close_connection(c);

return;

}

sscf = ngx_http_get_module_srv_conf(hc->conf_ctx,

ngx_http_ssl_module);

// 调用该函数生成ssl

if (ngx_ssl_create_connection(&sscf->ssl, c, NGX_SSL_BUFFER)

!= NGX_OK)

{

ngx_http_close_connection(c);

return;

}

}

}

...

}

ngx_int_t

ngx_ssl_create_connection(ngx_ssl_t *ssl, ngx_connection_t *c, ngx_uint_t flags)

{

...

sc->session_ctx = ssl->ctx;

sc->connection = SSL_new(ssl->ctx);

if (sc->connection == NULL) {

ngx_ssl_error(NGX_LOG_ALERT, c->log, 0, "SSL_new() failed");

return NGX_ERROR;

}

if (SSL_set_fd(sc->connection, c->fd) == 0) {

ngx_ssl_error(NGX_LOG_ALERT, c->log, 0, "SSL_set_fd() failed");

return NGX_ERROR;

}

...

}

第一次收到client hello之后,完成初始化后调用ngx_ssl_handshake,其调用openssl的ssl_do_handshake

ngx_int_t

ngx_ssl_handshake(ngx_connection_t *c)

{

...

n = ngx_ssl_handshake_early_data(c);

n = SSL_do_handshake(c->ssl->connection);

...

}

调用keyless模块的init函数先是获取coremodule的main conf,然后获取到servers,遍历这些servers的上下文中的srv conf配置,然后把sscf→ssl.ctx设置cert_cb为keyless_cert_handler,这个函数在api使用证书的时候会调用。

static ngx_int_t

ngx_http_ssl_keyless_init(ngx_conf_t *cf)

{

...

cmcf = ngx_http_conf_get_module_main_conf(cf, ngx_http_core_module);

cscfp = cmcf->servers.elts;

for (s = 0; s < cmcf->servers.nelts; s++) {

sscf = cscfp[s]->ctx->srv_conf[ngx_http_ssl_module.ctx_index];

kscf = cscfp[s]->ctx->srv_conf[ngx_http_ssl_keyless_module.ctx_index];

if (sscf->enable == 1 && kscf->enable == 1) {

//TODO set prev cert callback handler.

kscf->prev_ssl_cert_cb = ngx_http_lua_ssl_cert_handler;

#endif

SSL_CTX_set_cert_cb(sscf->ssl.ctx, ngx_http_ssl_keyless_cert_handler, NULL);

}

...

}

cert handler做了了很多事情,初始化了很多nginx keyless相关的参数,核心在于这个函数新建了一条通向keyserver的连接。

static int

ngx_http_ssl_keyless_cert_handler(ngx_ssl_conn_t *ssl_conn, void *data)

{

...

if (ngx_http_ssl_keyless_get_keyserver_pc(klss) != NGX_OK) {

ngx_log_error(NGX_LOG_ERR, c->log, 0, "init keyless sess");

}

klss_state->data = klss;

klss_state->state = KEYLESS_STATE_RSA_INIT;

c->klss = klss;

pc = klss->pc;

rc = ngx_event_connect_peer(klss->pc);

pcc = ((ngx_peer_connection_t *)klss->pc)->connection;

pcc->data = klss;

pcc->write->handler = ngx_http_ssl_keyless_keyserver_handler;

pcc->read->handler = ngx_http_ssl_keyless_keyserver_handler;

...

}

static ngx_int_t

ngx_http_ssl_keyless_get_keyserver_pc(ngx_http_keyless_sess_t *klss)

{

...

pc->get = ngx_http_ssl_keyless_peer_get;

pc->free = ngx_http_ssl_keyless_peer_free;

...

}

do handshake的时候调用的是openssl的async job的库,相当于新开一个函数栈

ASYNC JOB

int SSL_do_handshake(SSL *s)

{

...

if (SSL_in_init(s) || SSL_in_before(s)) {

if ((s->mode & SSL_MODE_ASYNC) && ASYNC_get_current_job() == NULL) {

struct ssl_async_args args;

args.s = s;

ret = ssl_start_async_job(s, &args, ssl_do_handshake_intern);

} else {

ret = s->handshake_func(s);

}

}

...

}

int ASYNC_start_job(ASYNC_JOB **job, ASYNC_WAIT_CTX *wctx, int *ret,

int (*func)(void *), void *args, size_t size)

{

...

/* Start a new job */

if ((ctx->currjob = async_get_pool_job()) == NULL)

return ASYNC_NO_JOBS;

...

}

static ASYNC_JOB *async_get_pool_job(void) {

...

if (job == NULL) {

/* Pool is empty */

if ((pool->max_size != 0) && (pool->curr_size >= pool->max_size))

return NULL;

job = async_job_new();

if (job != NULL) {

if (! async_fibre_makecontext(&job->fibrectx)) {

async_job_free(job);

return NULL;

}

pool->curr_size++;

}

}

...

}

先做初始化get下context,malloc一个stack,这个堆栈创建完成后把函数放进去,使用makecontext来创建一旦调用就会运行该函数,async_start_func本身使用当前job中的func,函数也是传进来的参数

int async_fibre_makecontext(async_fibre *fibre)

{

fibre->env_init = 0;

if (getcontext(&fibre->fibre) == 0) {           //初始化当前ucontext

fibre->fibre.uc_stack.ss_sp = OPENSSL_malloc(STACKSIZE);

if (fibre->fibre.uc_stack.ss_sp != NULL) {

fibre->fibre.uc_stack.ss_size = STACKSIZE;

fibre->fibre.uc_link = NULL;

makecontext(&fibre->fibre, async_start_func, 0);

return 1;

}

} else {

fibre->fibre.uc_stack.ss_sp = NULL;

}

return 0;

}

Pause job最关键的是swapcontext,这个在func中一旦被调用的话,就可以立即切换栈信息,切回start_job的主函数,根据job→status=ASYNC_JOB_PAUSING来返回

int ASYNC_pause_job(void)

{

...

if (!async_fibre_swapcontext(&job->fibrectx,

&ctx->dispatcher, 1)) {

ASYNCerr(ASYNC_F_ASYNC_PAUSE_JOB, ASYNC_R_FAILED_TO_SWAP_CONTEXT);

return 0;

}

...

}

切回主函数之后,因为start job是for死循环,所以会根据job的状态进行返回

int ASYNC_start_job(ASYNC_JOB **job, ASYNC_WAIT_CTX *wctx, int *ret,

int (*func)(void *), void *args, size_t size)

{

...

for (;;) {

if (ctx->currjob != NULL) {

if (ctx->currjob->status == ASYNC_JOB_PAUSING) {

*job = ctx->currjob;

ctx->currjob->status = ASYNC_JOB_PAUSED;

ctx->currjob = NULL;

return ASYNC_PAUSE;

}

if (ctx->currjob->status == ASYNC_JOB_PAUSED) {

ctx->currjob = *job;

/* Resume previous job */

if (!async_fibre_swapcontext(&ctx->dispatcher,

&ctx->currjob->fibrectx, 1)) {

ASYNCerr(ASYNC_F_ASYNC_START_JOB,

ASYNC_R_FAILED_TO_SWAP_CONTEXT);

goto err;

}

continue;

}

...

}

static int ssl_start_async_job(SSL *s, struct ssl_async_args *args,

int (*func) (void *))

{

...

switch (ASYNC_start_job(&s->job, s->waitctx, &ret, func, args,

sizeof(struct ssl_async_args))) {

case ASYNC_ERR:

s->rwstate = SSL_NOTHING;

SSLerr(SSL_F_SSL_START_ASYNC_JOB, SSL_R_FAILED_TO_INIT_ASYNC);

return -1;

case ASYNC_PAUSE:

s->rwstate = SSL_ASYNC_PAUSED;

return -1;

}

...

}

返回的这个状态码,在nginx里面接到就是SSL_ERROR_WANT_ASYNC

关键就是调用async_pause_job交还给nginx来做keyless处理,以及将与keyserver的状态调整为presend。

int kls_rsa_private_decrypt(int flen, const unsigned char *from, unsigned char *to, RSA *rsa, int padding)

{

...

waitctx = ASYNC_get_wait_ctx(job);

ASYNC_WAIT_CTX_get_fd(waitctx, (void *)waitctx, &fd, (void *)&klss_state);

if (klss_state->state == KEYLESS_STATE_RSA_INIT) {

klss_state->is_rsa_decrypt = 1;

dec_ctx = &klss_state->dec_ctx;

dec_ctx->from = from;

dec_ctx->to = to;

dec_ctx->flen = flen;

dec_ctx->padding = padding;

klss_state->state = KEYLESS_STATE_RSA_PRE_SEND;

ASYNC_pause_job();

}

...

}

这里处理完交还给nginx,之后nginx 就可以做原本由openssl实现的加解密。

重写engine重写两个关键函数

static int bind_helper(ENGINE *e)

{

kls_rsa_meth = RSA_meth_new("keyless rsa method", RSA_METHOD_FLAG_NO_CHECK);

RSA_meth_set_sign(kls_rsa_meth, kls_rsa_sign);

RSA_meth_set_priv_dec(kls_rsa_meth, kls_rsa_private_decrypt);

if (!ENGINE_set_id(e, engine_keyless_id) ||

!ENGINE_set_name(e, engine_keyless_name) ||

!ENGINE_set_RSA(e, kls_rsa_meth)) {

return 0;

}

return 1;

}

static ENGINE *ENGINE_keyless(void)

{

ENGINE *e = ENGINE_new();

if (e == NULL)

return NULL;

if (!bind_helper(e)) {

ENGINE_free(e);

return NULL;

}

return e;

}

void engine_load_keyless_int(void)

{

ENGINE *e = ENGINE_keyless();

if (e == NULL) {

return;

}

ENGINE_add(e);

ENGINE_free(e);

ERR_clear_error();

return;

}

重写的两个函数是sign和priv_dec

struct rsa_meth_st {

int (*rsa_priv_dec) (int flen, const unsigned char *from,

unsigned char *to, RSA *rsa, int padding);

int (*rsa_sign) (int type,

const unsigned char *m, unsigned int m_length,

unsigned char *sigret, unsigned int *siglen,

const RSA *rsa);

}

int kls_rsa_sign(int type, const unsigned char *m, unsigned int m_length, unsigned char *sigret, unsigned int *siglen, const RSA *rsa)

{

...

waitctx = ASYNC_get_wait_ctx(job);

ASYNC_WAIT_CTX_get_fd(waitctx, (void *)waitctx, &fd, (void *)&klss_state);

if (klss_state->state == KEYLESS_STATE_RSA_INIT) {

klss_state->is_rsa_sign = 1;

sign_ctx = &klss_state->sign_ctx;

sign_ctx = &klss_state->sign_ctx;

sign_ctx->type = type;

sign_ctx->m = m;

sign_ctx->m_length = m_length;

klss_state->state = KEYLESS_STATE_RSA_PRE_SEND;

ASYNC_pause_job();

}

...

}

调用pause时最重要逻辑是async_fibre_swapcontext,这个函数是用于切换的核心,同时进行初始化操作,把函数放到新开辟栈里运行。

static ossl_inline int async_fibre_swapcontext(async_fibre *o, async_fibre *n, int r)

{

o->env_init = 1;

if (!r || !_setjmp(o->env)) {

if (n->env_init)

_longjmp(n->env, 1);

else

setcontext(&n->fibre);

}

return 1;

}


文章来源: https://www.freebuf.com/articles/web/261776.html
如有侵权请联系:admin#unsafe.sh