该onvif模块目前仅支持WSS认证,我要对其进行修改,添加digest认证支持,目前已经做出完整修改,但是根据rtsp的digest鉴权流程,有没有什么能应用到我的框架里面的?比如说response的计算可不可以采用?比如说判断是否需要认证还有用户密码的获取能不能参考?还是说密码的获取应该像我已修改的一样参考WSS认证密码的获取?还有http头部的解析对我的修改有没有参考价值?请你详细地进行分析,可参考的地方请指出来怎么模仿,我目前的修改如下:
#### 流程梳理
- 初始化认证状态:`digest_authenticated`=0,`wss_authenticated`=0。
- 检查Digest凭据:
- 如果有,验证,成功则`digest_authenticated`=1;失败则返回401。
- 如果没有,继续。
- 解析SOAP消息(如果前面返回了,就无需解析)。
- 在解析SOAP之后,检查SOAP头中是否有`WSS`凭据:
- 如果有,进行`WSS`验证(调用原有的`soap_usernametoken_auth`等函数):
成功:`wss_authenticated`=1
失败:构造`SOAP Fault`,设置上下文返回类型为`SOAP_FAULT`,然后返回错误。
- 如果没有,`wss_authenticated`保持0。
- 检查是否至少有一个认证通过:if (`digest_authenticated` || `wss_authenticated`)
- 如果是,则继续后续处理(调用`soap_serve_request`)。
- 否则,返回401(带`WWW-Authenticate`头)。
**认证组合测试**:
| 测试用例 | 预期结果 |
| ------------------------ | -------------- |
| 有效`Digest` + 无`WSS` | 200 OK |
| 有效`Digest` + 有`效WSS` | 200 OK |
| 有效`Digest` + 无效`WSS` | 400/SOAP Fault |
| 无效`Digest` + 有效`WSS` | 401 |
| 无效`Digest` + 无效`WSS` | 401 |
| 无`Digest` + 有效`WSS` | 200 OK |
| 无`Digest` + 无效`WSS` | 400/SOAP Fault |
| 无`Digest` + 无`WSS` | 401 |
#### 修改步骤
- 修改CONTEXT结构体,增加一些字段来存储Digest认证相关的信息。
- 对`http_parse.c`中解析HTTP头部的函数,增加对Authorization头的解析
- 实现Digest认证验证函数
- 生成nonce和管理nonce,比如设置nonce的过期时间,防止重放攻击
- 修改`onvif_proc_data_srv`函数(`onvif_srv.c`),在解析SOAP之前,先检查是否有Digest认证头并进行验证。
- 在401响应的HTTP报头添加`WWW-Authenticate`头。
#### HTTP解析流程
在`http_parser.c`中,HTTP报文的解析主要由`http_parser()`函数完成。该函数通过以下步骤将报文内容读入CONTEXT结构体:
##### 1. 初始化CONTEXT结构体
- 分配头部缓冲区(`context->head_buf`),大小为`HTTP_HEAD_BUF_SIZE`。
- 初始化CONTEXT中的各个字段,包括`head_end`(指向缓冲区当前写入位置)、`hbuf_data_end`(指向已接收数据的末尾)等。
- 设置套接字为非阻塞模式。
##### 2. 解析HTTP头部
- 调用`http_parse_header(context)`函数解析HTTP头部。
- 读取请求行(第一行):调用`http_read_line`读取一行,然后通过`http_parse_method`解析HTTP方法(GET/POST等),`http_parse_url`解析URL(包括路径和查询参数),`http_parse_version`解析HTTP版本。
- 解析请求头字段:循环调用`http_read_line`读取每一行,直到遇到空行(表示头部结束)。每行按`key: value`格式解析,通过`http_parse_key`函数处理。该函数会调用注册的关键字解析器(如`http_parse_host`、`http_parse_content_type`等)将对应的值存入CONTEXT的字段中。
- 例如,Host字段的值会存入`context->host`。
##### 3. 解析HTTP内容(Body)
- 如果有内容(如POST请求),根据`Content-Length`或`Transfer-Encoding`处理:
- 对于普通POST请求,调用`http_read_content`函数读取内容到`context->content_buf`。
- 对于文件上传(`multipart/form-data`),会处理`boundary`,并调整内容指针。
- 读取的内容会被存入`context->content_buf`,长度由`context->content_len`记录。
##### 4. 后续处理
- 根据解析出的URL路径,确定服务组(如`ONVIF`或`HTTPD`),并调用对应的`data_srv`函数进行处理。
- 处理结果会被放入响应缓冲区,通过`http_make_response`或`http_make_file_response`发送。
#### **步骤一:修改CONTEXT结构体(httpd.h)**
在CONTEXT结构体中增加Digest认证相关的字段:
```c
#define LEN_USERNAME 256
#define LEN_REALM 256
#define LEN_DIGEST_NONCE 256
#define LEN_URI 256
#define LEN_RESPONSE 256
#define LEN_ALGORITHM 64
#define LEN_QOP 64
#define LEN_NC 32
#define LEN_CNONCE 64
#define LEN_OPAQUE 256
typedef struct _CONTEXT
{
/* 现有字段... */
/* 新增Digest认证相关字段 */
BOOL digest_authenticated; /* Digest认证是否通过 */
BOOL wss_authenticated; /* Digest认证是否通过 */
char generate_realm[LEN_REALM]; /* 生成的realm */
char generate_opaque[LEN_OPAQUE]; /* 生成的opaque */
char digest_username[LEN_USERNAME]; /* Digest认证用户名 */
char digest_realm[LEN_REALM]; /* Digest认证域 */
char digest_nonce[LEN_DIGEST_NONCE]; /* 服务器生成的nonce */
char digest_uri[LEN_URI]; /* 请求的URI */
char digest_response[LEN_RESPONSE]; /* 客户端计算的响应值 */
char digest_algorithm[LEN_ALGORITHM];/* 算法类型 */
char digest_qop[LEN_QOP]; /* 保护质量 */
char digest_nc[LEN_NC]; /* 随机数计数器 */
char digest_cnonce[LEN_CNONCE]; /* 客户端随机数 */
char digest_opaque[LEN_OPAQUE]; /* 服务器不透明数据 */
/* 其他现有字段... */
} CONTEXT;
```
#### 步骤二:解析Authorization头(`http_parser.c`)
**添加Authorization头解析器**
在`http_parser_init()`函数中添加:
```c
void http_parser_init()
{
/* 现有初始化代码... */
/* 添加Authorization头解析器 */
http_key_parser_add("Authorization", http_parse_authorization);
/* ... */
}
```
**实现Authorization头解析函数**
参考`http`头格式:
```c
GET/cgi-bin/checkout?a=b HTTP/1.1
Authorization: Digest
username="Mufasa",
realm="realm",
nonce="dcd98b7102dd2f0e8b11d0f600bfb0c0",
uri="/xxxx/System/Register",
response="6629fae49393a05397450978507c4ef1",
algorithm=MD5,
qop=auth,
nc=00000001,
cnonce="0a4f113b",
/* opaque="5ccc069c403ebaf9f0171e9517f40e41" */
```
辅助函数:`trim_whitespace`
- 去除键前后的空格,避免因空格导致匹配失败。
- 十六进制字符转数值
```c
/* 移除字符串两端的空格 */
void trim_whitespace(char *str) {
char *end;
/* 去除前导空格 */
while (isspace((unsigned char)*str)) str++;
/* 全空格字符串处理 */
if (*str == 0) return;
/* 去除尾部空格 */
end = str + strlen(str) - 1;
while (end > str && isspace((unsigned char)*end)) end--;
*(end + 1) = '\0';
}
/* 十六进制字符转数值 */
int hex_val(char c) {
return isdigit(c) ? c - '0' : tolower(c) - 'a' + 10;
}
```
**`http_parse_authorization`:**
该函数假设各参数之间使用逗号分隔,且参数值中不包含逗号。
```c
LOCAL S32 http_parse_authorization(CONTEXT *context, char *value)
{
char *parse_ptr = value;
char *token;
int error_count = 0;
/* 检查Digest认证类型 */
if (strncasecmp(parse_ptr, "Digest ", 7) != 0) {
return OK;
}
parse_ptr += 7; /* 跳过前缀 */
char *saveptr; /* strtok_r上下文指针 */
token = strtok_r(parse_ptr, ",", &saveptr);
while (token != NULL) {
/* 分割键值对 */
char *eq = strchr(token, '=');
if (!eq) {
HTTPD_DEBUG("Invalid param format: %s", token);
error_count++;
token = strtok_r(NULL, ",", &saveptr);
continue;
}
/* 分割键值 */
*eq = '\0';
char *key = token;
char *val = eq + 1;
/* 移除键值两端的空格 */
trim_whitespace(key);
trim_whitespace(val);
/* 处理引号包裹的值 */
size_t vlen = strlen(val);
if (vlen >= 2 && val[0] == '"' && val[vlen-1] == '"') {
val[vlen-1] = '\0'; /* 移除尾部引号 */
memmove(val, val+1, vlen-1); /* 移除头部引号 */
vlen -= 2; /* 更新长度 */
}
/* 存储到CONTEXT字段 */
if (strcasecmp(key, "username") == 0) {
strncpy(context->digest_username, val, LEN_USERNAME-1);
}
else if (strcasecmp(key, "realm") == 0) {
strncpy(context->digest_realm, val, LEN_REALM-1);
}
else if (strcasecmp(key, "nonce") == 0) {
strncpy(context->digest_nonce, val, LEN_DIGEST_NONCE-1);
}
else if (strcasecmp(key, "uri") == 0) {
/* 规范化URI路径(移除查询参数)*/
char *query_start = strchr(val, '?');
if (query_start) *query_start = '\0';
strncpy(context->digest_uri, val, LEN_URI-1);
}
else if (strcasecmp(key, "response") == 0) {
strncpy(context->digest_response, val, LEN_RESPONSE-1);
}
else if (strcasecmp(key, "algorithm") == 0) {
strncpy(context->digest_algorithm, val, LEN_ALGORITHM-1);
}
else if (strcasecmp(key, "qop") == 0) {
strncpy(context->digest_qop, val, LEN_QOP-1);
}
else if (strcasecmp(key, "nc") == 0) {
strncpy(context->digest_nc, val, LEN_NC-1);
}
else if (strcasecmp(key, "cnonce") == 0) {
strncpy(context->digest_cnonce, val, LEN_CNONCE-1);
}
else if (strcasecmp(key, "opaque") == 0) {
strncpy(context->digest_opaque, val, LEN_OPAQUE-1);
}
else {
HTTPD_DEBUG("Unknown Digest param: %s=%s", key, val);
}
token = strtok_r(NULL, ",", &saveptr);
}
/* 检查必需参数是否齐全 */
if (strlen(context->digest_response) == 0) {
HTTPD_WARNING("Missing required response field");
return ERROR;
}
return (error_count > 3) ? ERROR : OK;
}
```
#### 步骤三:生成响应Authorization头
`onvif_srv.c`文件,`onvif_make_response(CONTEXT *context)`函数会在状态码非200时调用`http_make_response()`函数发送HTTP错误响应。
```c
#define HTTP_UNAUTHORIZED 401
LOCAL S32 onvif_make_response(CONTEXT *context)
{
/* 其他字段... */
if (context->code == HTTP_REQ_OK)
{
if (soap->use_udp == FALSE &&
soap->xml_buf.start != NULL &&
soap->xml_buf.start != soap->xml_buf.last)
{
ONVIF_TRACE("Onvif_make_response return onvif_send_http_rsp_packet.");
ret = onvif_send_http_rsp_packet(soap);
goto out;
}
else
{
ONVIF_TRACE("Onvif_make_response return OK.");
ret = OK;
goto out;
}
}
ONVIF_TRACE("Onvif_make_response return http_make_response.");
ret = http_make_response(context);
out:
/* 清除content内容 */
http_reset_context(context);
return ret;
}
```
在`http_make_response()`函数中,通过`http_send_rsp_header()`设置HTTP响应报文头部。因此,需要修改`http_send_rsp_header()`函数,使其能够在HTTP状态码为401时添加`WWW-Authenticate`头部。
```c
/* Location */
if (HTTP_MOVE_TEMPORARILY == context->code) {
// ...原有Location代码...
}
/* ========== 新增Digest认证头 ========== */
if (HTTP_UNAUTHORIZED == context->code) {
/* 必需字段: realm, nonce, qop */
context->digest_realm = “Restricted Area”;
/* 生成带时效的nonce */
if (generate_digest_nonce(context) != OK) {
return ERROR;
}
length = snprintf(context->head_end, (context->head_buf_end - context->head_end),
"WWW-Authenticate: Digest realm=\"Restricted Area\", nonce=\"%s\", qop=\"auth\"", context->digest_nonce);
/* 结束头部 */
length += snprintf(context->head_end + length, (context->head_buf_end - context->head_end - length), "\r\n");
context->head_end += length;
context->header_len += length;
}
/* Access-Control-Allow-Origin */
if (context->origin == TRUE) {
/* ...原有代码... */
}
```
#### 步骤四:nonce生成及过期管理:
Rand_Byte实现:
在tpssl中定义了RandBytes,但是return 0;
```c
int tpssl_RandBytes(unsigned char* buf, int num)
{
return 0;// 接口已无效,待处理
}
```
在`tpssl_rsa_encrypt`和其他函数中,使用了`mbedtls_ctr_drbg_random`作为随机数生成器,但这是通过`mbedtls`的上下文来生成随机数的,并不是直接可以调用的函数。
在`mbedtls`中,生成随机数的常用函数是`mbedtls_ctr_drbg_random`。但是,这个函数需要一个`mbedtls_ctr_drbg_context`上下文,而在代码中并没有一个直接封装好的函数来生成随机字节。
但是,在多个函数中(例如`tpssl_rsa_encrypt`)都使用了`mbedtls_ctr_drbg_random`,因此,可以自己封装一个类似于`RandBytes`的函数,但需要确保已经初始化了随机数生成器。
在`tpssl_private_decrypt`函数中,有一个使用随机数生成器的例子:
```c
mbedtls_ctr_drbg_init( &ctr_drbg );
mbedtls_entropy_init( &entropy );
ret = mbedtls_ctr_drbg_seed( &ctr_drbg, mbedtls_entropy_func, &entropy, (const unsigned char *) pers, strlen( pers ) );
...
/* 然后使用 mbedtls_ctr_drbg_random 生成随机数 */
```
所以,如果需要生成随机数,可以按照上述步骤初始化并使用`mbedtls_ctr_drbg_random`。
```c
int tpssl_SecureRandBytes(unsigned char* buf, int len) {
static mbedtls_ctr_drbg_context drbg;
static mbedtls_entropy_context entropy;
static int initialized = 0;
const char pers[] = "tpssl_rand_seed";
if (!initialized) {
mbedtls_entropy_init(&entropy);
mbedtls_ctr_drbg_init(&drbg);
if (mbedtls_ctr_drbg_seed(&drbg, mbedtls_entropy_func, &entropy,
(unsigned char*)pers, sizeof(pers)-1) != 0) {
return 0; /* 失败返回0 */
}
initialized = 1;
}
if (mbedtls_ctr_drbg_random(&drbg, buf, len) != 0) {
return 0; /* 生成随机数失败 */
}
return 1; /* 成功返回1 */
}
```
可实现:
```c
#include <time.h>
#include <string.h>
#define ERROR_CRYPTO_FAIL -1
#define ERROR_BUFFER_OVERFLOW -2
#define OK 0
#define NONCE_LIFETIME 30
S32 generate_digest_nonce(CONTEXT *context) {
/* 数据源:时间戳+随机数 */
struct {
time_t timestamp; /* 精确到秒的时间戳 */
time_t expiration_time; /* 过期时间 */
unsigned char rand_bytes[16]; /* 强随机数 */
} nonce_seed;
/* 取当前时间戳 */
nonce_seed.timestamp = time(NULL);
nonce_seed.expiration_time = nonce_seed.timestamp + NONCE_LIFETIME; /* 过期时间 */
/* 生成密码学强随机数 (使用OpenSSL RAND_bytes) */
if (RAND_bytes(nonce_seed.rand_bytes, sizeof(nonce_seed.rand_bytes)) != 1) {
return ERROR_CRYPTO_FAIL; /* 随机数生成失败 */
}
/* 转为十六进制字符串格式 */
char hex_buffer[2 * sizeof(nonce_seed) + 1];
const unsigned char *ptr = (const unsigned char *)&nonce_seed;
for (size_t i = 0; i < sizeof(nonce_seed); i++) {
snprintf(hex_buffer + 2*i, 3, "%02x", ptr[i]);
}
hex_buffer[2 * sizeof(nonce_seed)] = '\0';
/* 组合realm和hex_buffer生成nonce:realm:hex_buffer" */
int len = snprintf(context->digest_nonce, LEN_DIGEST_NONCE, "%s:%s", context->generate_realm, hex_buffer);
if (len < 0 || len >= LEN_DIGEST_NONCE) {
return ERROR_BUFFER_OVERFLOW;
}
return OK;
}
/* NONCE验证函数(应在认证流程中调用) */
int validate_nonce(const char *nonce_str) {
/* 提取时间结构体部分(跳过realm前缀) */
const char *hex_seed = strchr(nonce_str, ':') + 1;
size_t seed_len = strlen(hex_seed)/2;
/* 将HEX转回二进制结构 */
struct {
time_t timestamp;
time_t expiration_time;
unsigned char rand_bytes[16];
} seed;
for (size_t i = 0; i < seed_len; i++) {
sscanf(hex_seed + 2*i, "%2hhx", (unsigned char*)&seed + i);
}
/* 过期判断 */
time_t current_time = time(NULL);
if (current_time > seed.expiration_time) {
return 0; /* 过期 */
}
return 1; /* 有效 */
}
```
#### 步骤五:修改`onvif_proc_data_srv`函数(`onvif_srv.c`)
在`onvif_proc_data_srv`函数中,在解析SOAP之前,先检查是否有Digest认证头,并进行验证。
```c
S32 onvif_proc_data_srv(CONTEXT *context)
{
/* 现有代码(初始化和HTTP检查)... */
/* ==== 新增Digest认证检查 ==== */
/* 初始化认证状态 */
context->digest_authenticated = FALSE;
context->wss_authenticated = FALSE;
/* 检查是否有Digest认证头以及对realm、nonce进行校验 */
if (context->digest_username[0] != '\0'&& strcmp(context->digest_realm,context->generate_realm) == 0 && validate_nonce(context->digest_nonce)==1)
{
/* 执行Digest认证 */
if (check_digest_auth(context) == OK)
{
context->digest_authenticated = TRUE;
ONVIF_TRACE("Digest authentication successful");
}
else
{
ONVIF_TRACE("Digest authentication failed");
/* 认证失败,返回401 */
context->code = HTTP_UNAUTHORIZED;
return ERROR;
}
}
/* 继续原有处理逻辑(端口处理和XML解析)... */
/* ==== 新增WSS认证检查 ==== */
if (soap->has_header) {
if (soap_usernametoken_auth(soap, USER_TYPE_ADMIN) != OK) {
ONVIF_ERROR("WSS authentication failed");
soap_fault(soap, "SOAP-ENV:Sender", "ter:NotAuthorized", NULL,
"Authentication failed");
soap->error = SOAP_FAULT;
context->wss_authenticated = FALSE;
} else {
context->wss_authenticated = TRUE;
ONVIF_TRACE("WSS authentication successful");
}
}
/* ==== 最终认证检查 ==== */
if (!context->digest_authenticated && !context->wss_authenticated) {
ONVIF_TRACE("No valid authentication provided");
context->code = HTTP_UNAUTHORIZED;
return ERROR;
}
/* ==== 请求处理(原有逻辑)... */
return OK;
}
```
#### 步骤六:实现Digest认证验证函数
实现一个函数,用于验证Digest认证的响应是否正确。
```c
extern char g_ciphertext[];
S32 check_digest_auth(CONTEXT *context) {
if (!context) return -1;
/* 验证必要参数存在 */
if (strlen(context->digest_username) == 0 ||
strlen(context->digest_realm) == 0 ||
strlen(context->digest_nonce) == 0 ||
strlen(context->digest_uri) == 0 ||
strlen(context->digest_response) == 0) {
ONVIF_WARN("Missing required Digest auth parameters");
return -1;
}
/* 获取用户对应的密码 */
char stored_pwd[USER_MANAGEMENT_PASSWD_MAX_STR_LEN + 1] = {0};
if (strcmp(context->digest_username, TP_ROOT_USER) == 0) {
/* 管理员账户 */
char *plain_pwd = tpssl_rsa_decrypt(PRI_KEY_FILE_PATH,
(U8 *)g_ciphertext,,
(U32)strlen(g_ciphertext));
if (!plain_pwd) return -1;
strncpy(stored_pwd, plain_pwd, sizeof(stored_pwd)-1);
ONVIF_FREE(plain_pwd);
} else {
/* 第三方账户 */
THIRD_ACCOUNT_USER_MANAGEMENT third_acct;
if (0 == ds_read(THIRD_ACCOUNT_PATH, &third_acct, sizeof(third_acct))) {
char *plain_pwd = tpssl_rsa_decrypt(PRI_KEY_FILE_PATH,
(U8 *)third_acct.ciphertext,
(U32)strlen(third_acct.ciphertext));
if (!plain_pwd) return -1;
strncpy(stored_pwd, plain_pwd, sizeof(stored_pwd)-1);
ONVIF_FREE(plain_pwd);
} else {
return -1;
}
}
/* 计算HA1 = MD5(username:realm:password) */
char ha1_input[256];
snprintf(ha1_input, sizeof(ha1_input), "%s:%s:%s",
context->digest_username, context->digest_realm, stored_pwd);
char ha1[33];
tpssl_md5_hex(ha1_input, ha1);
memset(stored_pwd, 0, sizeof(stored_pwd)); /* 立即清除密码 */
/* 计算HA2 = MD5(method:uri) */
char ha2_input[256];
snprintf(ha2_input, sizeof(ha2_input), "%s:%s",
http_method_str(context->method), context->digest_uri);
char ha2[33];
tpssl_md5_hex(ha2_input, ha2);
/* 计算预期响应 = MD5(HA1:nonce:nc:cnonce:qop:HA2) */
char resp_input[512];
snprintf(resp_input, sizeof(resp_input), "%s:%s:%s:%s:%s:%s",
ha1, context->digest_nonce, context->digest_nc,
context->digest_cnonce, context->digest_qop, ha2);
char expected_resp[33];
tpssl_md5_hex(resp_input, expected_resp);
/* 验证响应值 */
if (strcasecmp(expected_resp, context->digest_response) == 0) {
ONVIF_TRACE("Digest auth success for user: %s", context->digest_username);
return 0; /* 认证成功 */
}
ONVIF_WARN("Digest auth failed. Expected: %s, Received: %s",
expected_resp, context->digest_response);
return -1; /* 认证失败 */
}
/* 使用tpssl实现的MD5计算(十六进制输出) */
void tpssl_md5_hex(const char *input, char *output) {
if (!input || !output) return;
Md5 ctx;
unsigned char digest[16]; // MD5_DIGEST_LENGTH=16
tpssl_InitMd5(&ctx);
tpssl_Md5Update(&ctx, (const byte*)input, strlen(input));
tpssl_Md5Final(&ctx, digest);
for (int i = 0; i < 16; i++) {
sprintf(output + (i * 2), "%02x", digest[i]);
}
output[32] = '\0';
}
/* 将HTTP方法枚举转换为字符串 */
const char *http_method_str(HTTP_METHOD method) {
switch(method) {
case HTTP_METHOD_GET: return "GET";
case HTTP_METHOD_POST: return "POST";
/* 添加PUT和DELETE(如果枚举中有定义)*/
#if defined(INCLUDE_UPNP_SERVER)
case HTTP_METHOD_MGET: return "MGET";
case HTTP_METHOD_MPOST: return "MPOST";
case HTTP_METHOD_SUBSCRIBE: return "SUBSCRIBE";
case HTTP_METHOD_UNSUBSCRIBE:return "UNSUBSCRIBE";
#endif
case HTTP_METHOD_UNKOWN: return "UNKNOWN";
default: return "INVALID";
}
}
```
最新发布