1. TLS的背景
在SSL/TLS出现之前,很多应用层协议(http、ftp、smtp等)都存在着网络安全问题,例如大家所熟知的http协议,在传输过程中使用的是明文信息,传输报文一旦被截获便会泄露传输内容;传输过程中报文如果被篡改,无法轻易发现;无法保证消息交换的对端身份的可靠性。为了解决此类问题,人们在应用层和传输层之间加入了SSL/TLS协议。
如下图所示,HTTP/1.1 协议默认是以明文方式传输数据的,这就带来三个风险:窃听风险、伪装风险、篡改风险。HTTP 协议自身没有加密机制,但可以通过和 TLS (Transport Layer Security) / SSL (Secure Socket Layer) 的组合使用,加密 HTTP 的通信内容,借助 TLS / SSL 提供的信息加密功能、完整性校验功能、身份验证功能保障网络通信的安全,与 TLS / SSL 组合使用的 HTTP 被称为 HTTPS(HTTPSecure),可以说 HTTPS 相当于身披 SSL / TLS 外壳的 HTTP。
HTTPS协议分层模型2. 什么是TLS
TLS(Transport Layer Security,安全传输层),TLS是建立在传输层TCP协议之上的协议,服务于应用层,它的前身是SSL(Secure Socket Layer,安全套接字层),它实现了将应用层的报文进行加密后再交由TCP进行传输的功能。
2.1 TLS的作用
TLS协议主要解决如下三个网络安全问题。
保密(message privacy),保密通过加密encryption实现,所有信息都加密传输,第三方无法嗅探;
完整性(message integrity),通过MAC校验机制,一旦被篡改,通信双方会立刻发现;
认证(mutual authentication),双方认证,双方都可以配备证书,防止身份被冒充; 2.2 TLS的发展过程
1995: SSL 2.0, 由Netscape提出,这个版本由于设计缺陷,并不安全,很快被发现有严重漏洞,已经废弃。
1996: SSL 3.0. 写成RFC,开始流行。目前(2015年)已经不安全,必须禁用。
1999: TLS 1.0. 互联网标准化组织ISOC接替NetScape公司,发布了SSL的升级版TLS 1.0版
2006: TLS 1.1. 作为 RFC 4346 发布。主要修复了CBC模式相关的如BEAST攻击等漏洞
2008: TLS 1.2. 作为 RFC 5246 发布 。增进安全性,目前应该主要部署的版本
2018: TLS 1.3 作为 RFC 8446 发布 ,大幅增进安全性
SSL/TLS协议发布时间与网站支持率对比2.3 TLS 报文结构
TLS (Transport Layer Security)协议是由TLS 记录协议(TLS Record Protocol)和TLS 握手协议(TLS Handshake Protocol)这两层协议叠加而成的,位于底层的TLS 记录协议负责进行信息传输和认证加密,位于上层的TLS 握手协议则负责除加密以外的其它各种操作,比如密钥协商交换等。上层的TLS 握手协议又可以分为4个子协议,TLS 协议的层次结构如下图所示:
TLS协议的层次结构
前面已经介绍了TLS 协议的认证加密算法,这里先介绍TLS 记录协议的报文结构,每一条TLS 记录以一个短标头起始,标头包含记录内容的类型(或子协议)、协议版本和长度,消息数据紧跟在标头之后,如下图所示:
TLS记录报文
了解了TLS 记录报文结构,前面也介绍了认证加密过程,下面以AES-GCM为例,看看密文(消息明文AES-CNT加密而来,当不需要对消息加密时,此处保持明文即可)和MAC(将序列号、标头等附加信息与密文一起通过Galois-MAC计算而来)是如何添加进TLS 记录报文的:上图中的几个字段作用如下:
序列号:任一端都有自身的序列号并跟踪来自另一端TLS记录的数量,能确保消息不被重放攻击;
标头:将标头作为计算GMAC输入的一部分,能确保未进行加密的标头不会遭受篡改;
nonce:在加密通信中仅使用一次的密钥,比如前面介绍过的 IV(Initialization Vector)或CTR初始值。
3. RT-Thread使用TLS
目前常用的 TLS 方式:MbedTLS、OpenSSL、s2n 等,但是对于不同的加密方式,需要使用其指定的加密接口和流程进行加密,对于部分应用层协议的移植较为复杂。因此 SAL TLS 功能产生,主要作用是提供 Socket 层面的 TLS 加密传输特性,抽象多种 TLS 处理方式,提供统一的接口用于完成 TLS 数据交互。
3.1 RT-Thread SAL TLS 功能使用方式
使用流程如下:
配置开启任意网络协议栈支持(如 lwIP 协议栈);
配置开启 MbedTLS 软件包(目前RT-Thread只支持 MbedTLS 类型加密方式);
配置开启 SAL_TLS 功能支持(如下配置选项章节所示);
配置完成之后,只要在 socket 创建时传入的 protocol 类型使用 PROTOCOL_TLS 或 PROTOCOL_DTLS ,即可使用标准 BSD Socket API 接口,完成 TLS 连接的建立和数据的收发。示例代码如下所示:#include
#include
#include
#include
#include
/* RT-Thread 官网,支持 TLS 功能 */
#define SAL_TLS_HOST "www.rt-thread.org"
#define SAL_TLS_PORT 443
#define SAL_TLS_BUFSZ 1024
static const char *send_data = "GET /download/rt-thread.txt HTTP/1.1
"
"User-Agent: rtthread/4.0.1 rtt
";
void sal_tls_test(void)
{
int ret, i;
char *recv_data;
struct hostent *host;
int sock = -1, bytes_received;
struct sockaddr_in server_addr;
/* 通过函数入口参数url获得host地址(如果是域名,会做域名解析) */
host = gethostbyname(SAL_TLS_HOST);
recv_data = rt_calloc(1, SAL_TLS_BUFSZ);
if (recv_data == RT_NULL)
{
rt_kprintf("No memory
");
return;
}
/* 创建一个socket,类型是SOCKET_STREAM,TCP 协议, TLS 类型 */
if ((sock = socket(AF_INET, SOCK_STREAM, PROTOCOL_TLS)) < 0)
{
rt_kprintf("Socket error
");
goto __exit;
}
/* 初始化预连接的服务端地址 */
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(SAL_TLS_PORT);
server_addr.sin_addr = *((struct in_addr *)host->h_addr);
rt_memset(&(server_addr.sin_zero), 0, sizeof(server_addr.sin_zero));
if (connect(sock, (struct sockaddr *)&server_addr, sizeof(struct sockaddr)) < 0)
{
rt_kprintf("Connect fail!
");
goto __exit;
}
/* 发送数据到 socket 连接 */
ret = send(sock, send_data, strlen(send_data), 0);
if (ret <= 0)
{
rt_kprintf("send error,close the socket.
");
goto __exit;
}
/* 接收并打印响应的数据,使用加密数据传输 */
bytes_received = recv(sock, recv_data, SAL_TLS_BUFSZ - 1, 0);
if (bytes_received <= 0)
{
rt_kprintf("received error,close the socket.
");
goto __exit;
}
rt_kprintf("recv data:
");
for (i = 0; i < bytes_received; i++)
{
rt_kprintf("%c", recv_data);
}
__exit:
if (recv_data)
rt_free(recv_data);
if (sock >= 0)
closesocket(sock);
}
#ifdef FINSH_USING_MSH
#include
MSH_CMD_EXPORT(sal_tls_test, SAL TLS function test);
#endif /* FINSH_USING_MSH */
4. RT-Thread 基于SAL接口的TLS实现
4.1 TLS接口注册
TLS(Transport Layer Security)安全传输层协议主要用于通信数据的加密,并不影响SAL向上提供的接口。RT-Thread使用的TLS组件时mbedtls(一个由 ARM 公司使用 C 语言实现和维护的 SSL/TLS 算法库),如果启用了TLS组件,SAL层的实现函数中会自动调用mbedtls的接口函数,实现数据的加密传输。
RT-Thread的SAL接口的TLS协议的数据结构描述与需要向其注册的接口函数集合如下:
// .
t-threadcomponents
etsal_socketincludesal_tls.h
struct sal_proto_tls
{
char name[RT_NAME_MAX]; /* TLS protocol name */
const struct sal_proto_tls_ops *ops; /* SAL TLS protocol options */
};
struct sal_proto_tls_ops
{
int (*init)(void);
void* (*socket)(int socket);
int (*connect)(void *sock);
int (*send)(void *sock, const void *data, size_t size);
int (*recv)(void *sock, void *mem, size_t len);
int (*closesocket)(void *sock);
int (*set_cret_list)(void *sock, const void *cert, size_t size); /* Set TLS credentials */
int (*set_ciphersurite)(void *sock, const void* ciphersurite, size_t size); /* Set select ciphersuites */
int (*set_peer_verify)(void *sock, const void* peer_verify, size_t size); /* Set peer verification */
int (*set_dtls_role)(void *sock, const void *dtls_role, size_t size); /* Set role for DTLS */
};
sal_proto_tls对象的初始化和注册过程如下,(注册相关函数代码位于rt-threadcomponents
etsal_socketimplproto_mbedtls.c):
// .
t-threadcomponents
etsal_socketimplproto_mbedtls.c
static const struct sal_proto_tls_ops mbedtls_proto_ops=
{
RT_NULL,
mebdtls_socket,
mbedtls_connect,
(int (*)(void *sock, const void *data, size_t size)) mbedtls_client_write,
(int (*)(void *sock, void *mem, size_t len)) mbedtls_client_read,
mbedtls_closesocket,
};
static const struct sal_proto_tls mbedtls_proto =
{
"mbedtls",
&mbedtls_proto_ops,
};
int sal_mbedtls_proto_init(void)
{
/* register MbedTLS protocol options to SAL */
sal_proto_tls_register(&mbedtls_proto);
return 0;
}
INIT_COMPONENT_EXPORT(sal_mbedtls_proto_init);
// .
t-threadcomponents
etsal_socketsrcsal_socket.c
#ifdef SAL_USING_TLS
/* The global TLS protocol options */
static struct sal_proto_tls *proto_tls;
#endif
#ifdef SAL_USING_TLS
int sal_proto_tls_register(const struct sal_proto_tls *pt)
{
RT_ASSERT(pt);
proto_tls = (struct sal_proto_tls *) pt;
return 0;
}
#endif
变量proto_tls在文件sal_socket.c中属于全局变量(被static修饰,仅限于本文件内),该文件是SAL组件对外访问接口函数的实现文件。也就是说,我们启用TLS协议后,SAL组件对外提供的访问接口函数实现代码中就会调用TLS接口,完成数据的加密传输,而且该组件的注册是被自动初始化的,比较省心。
4.2 SAL对外提供的访问接口
SAL组件初始化并注册成功后,我们就可以使用SAL提供的接口进行应用开发了,先看看SAL组件提供了哪些访问接口:
// .
t-threadcomponents
etsal_socketincludesal_socket.h
int sal_accept(int socket, struct sockaddr *addr, socklen_t *addrlen);
int sal_bind(int socket, const struct sockaddr *name, socklen_t namelen);
int sal_shutdown(int socket, int how);
int sal_getpeername (int socket, struct sockaddr *name, socklen_t *namelen);
int sal_getsockname (int socket, struct sockaddr *name, socklen_t *namelen);
int sal_getsockopt (int socket, int level, int optname, void *optval, socklen_t *optlen);
int sal_setsockopt (int socket, int level, int optname, const void *optval, socklen_t optlen);
int sal_connect(int socket, const struct sockaddr *name, socklen_t namelen);
int sal_listen(int socket, int backlog);
int sal_recvfrom(int socket, void *mem, size_t len, int flags,
struct sockaddr *from, socklen_t *fromlen);
int sal_sendto(int socket, const void *dataptr, size_t size, int flags,
const struct sockaddr *to, socklen_t tolen);
int sal_socket(int domain, int type, int protocol);
int sal_closesocket(int socket);
int sal_ioctlsocket(int socket, long cmd, void *arg);
// .
t-threadcomponents
etsal_socketincludesal_netdb.h
struct hostent *sal_gethostbyname(const char *name);
int sal_gethostbyname_r(const char *name, struct hostent *ret, char *buf,
size_t buflen, struct hostent **result, int *h_errnop);
void sal_freeaddrinfo(struct addrinfo *ai);
int sal_getaddrinfo(const char *nodename,
const char *servname,
const struct addrinfo *hints,
struct addrinfo **res);
如果不习惯使用SAL层提供的sal_xxx形式的接口,SAL还为我们进行了再次封装,将其封装为比较通用的BSD Socket API,封装后的接口如下:
// .
t-threadcomponents
etsal_socketincludesocketsys_socketsyssocket.h
#ifdef SAL_USING_POSIX
int accept(int s, struct sockaddr *addr, socklen_t *addrlen);
int bind(int s, const struct sockaddr *name, socklen_t namelen);
int shutdown(int s, int how);
int getpeername(int s, struct sockaddr *name, socklen_t *namelen);
int getsockname(int s, struct sockaddr *name, socklen_t *namelen);
int getsockopt(int s, int level, int optname, void *optval, socklen_t *optlen);
int setsockopt(int s, int level, int optname, const void *optval, socklen_t optlen);
int connect(int s, const struct sockaddr *name, socklen_t namelen);
int listen(int s, int backlog);
int recv(int s, void *mem, size_t len, int flags);
int recvfrom(int s, void *mem, size_t len, int flags,
struct sockaddr *from, socklen_t *fromlen);
int send(int s, const void *dataptr, size_t size, int flags);
int sendto(int s, const void *dataptr, size_t size, int flags,
const struct sockaddr *to, socklen_t tolen);
int socket(int domain, int type, int protocol);
int closesocket(int s);
int ioctlsocket(int s, long cmd, void *arg);
#else
#define accept(s, addr, addrlen) sal_accept(s, addr, addrlen)
#define bind(s, name, namelen) sal_bind(s, name, namelen)
#define shutdown(s, how) sal_shutdown(s, how)
#define getpeername(s, name, namelen) sal_getpeername(s, name, namelen)
#define getsockname(s, name, namelen) sal_getsockname(s, name, namelen)
#define getsockopt(s, level, optname, optval, optlen) sal_getsockopt(s, level, optname, optval, optlen)
#define setsockopt(s, level, optname, optval, optlen) sal_setsockopt(s, level, optname, optval, optlen)
#define connect(s, name, namelen) sal_connect(s, name, namelen)
#define listen(s, backlog) sal_listen(s, backlog)
#define recv(s, mem, len, flags) sal_recvfrom(s, mem, len, flags, NULL, NULL)
#define recvfrom(s, mem, len, flags, from, fromlen) sal_recvfrom(s, mem, len, flags, from, fromlen)
#define send(s, dataptr, size, flags) sal_sendto(s, dataptr, size, flags, NULL, NULL)
#define sendto(s, dataptr, size, flags, to, tolen) sal_sendto(s, dataptr, size, flags, to, tolen)
#define socket(domain, type, protocol) sal_socket(domain, type, protocol)
#define closesocket(s) sal_closesocket(s)
#define ioctlsocket(s, cmd, arg) sal_ioctlsocket(s, cmd, arg)
#endif /* SAL_USING_POSIX */
// .
t-threadcomponents
etsal_socketincludesocket
etdb.h
struct hostent *gethostbyname(const char *name);
int gethostbyname_r(const char *name, struct hostent *ret, char *buf,
size_t buflen, struct hostent **result, int *h_errnop);
void freeaddrinfo(struct addrinfo *ai);
int getaddrinfo(const char *nodename,
const char *servname,
const struct addrinfo *hints,
struct addrinfo **res);
BSD Socket API也是我们进行网络应用开发时最常使用的接口,如果要使用这些接口,除了启用相应的宏定义,还需要包含这些接口所在的两个头文件。
5. RT-Thread TLS拓展
5.1 Wireshark 抓取TLS报文
5.1.1 配置抓包环境
1.SG105 Pro网关交换机配置
按照图片所示配置配置网关交换机的端口镜像功能,这样端口1连接PC,端口2连接开发板,开发板所有数据包都可以通过PC端的wireshark软件抓取。
2.PC端Wireshark配置
设置IP地址过滤(ip.addr == xxx.xxx.xxx.xxx,其中ip地址为开发板ip地址)
3.webclient抓包http协议,所有数据都是明文,没有加密 #include
#include
#define GET_HEADER_BUFSZ 1024
#define GET_RESP_BUFSZ 1024
#define GET_LOCAL_URI "http://www.rt-thread.com/service/rt-thread.txt"
/* send HTTP GET request by common request interface, it used to receive longer data */
static int webclient_get_comm(const char *uri)
3.webclient抓包https协议,密文数据 #include
#include
#define GET_HEADER_BUFSZ 1024
#define GET_RESP_BUFSZ 1024
#define GET_LOCAL_URI "https://www.rt-thread.com/service/rt-thread.txt"
/* send HTTP GET request by common request interface, it used to receive longer data */
static int webclient_get_comm(const char *uri)
原作者:windoufu