Digest_access_authentication 的基本原理

本文详细介绍了HTTP Digest Access Authentication的基本原理,并通过本地抓包测试进行实例演示。
http://en.wikipedia.org/wiki/Digest_access_authentication


基本原理:
1.客户端根据服务器端生成的nonce值 加上用户名和密码取MD5值,将这个值发送给服务器端,服务器端验证该值是否合法
具体请看http://en.wikipedia.org/wiki/Digest_access_authentication

HTTPLOOK本地抓包测试,服务器端用户名 tomcat,密码 tomcat
GET /club-test/IndexServlet HTTP/1.1
Accept: */*
Accept-Language: zh-cn
Accept-Encoding: gzip, deflate
User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; TCO_20100513102058; .NET CLR 2.0.50727; .NET CLR 3.0.04506.30; CIBA; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729)
Host: 127.0.0.1:8080
Connection: Keep-Alive


HTTP/1.1 401 Unauthorized
Server: Apache-Coyote/1.1
Pragma: No-cache
Cache-Control: no-cache
Expires: Thu, 01 Jan 1970 08:00:00 CST
WWW-Authenticate: Digest realm="Basic Authentication Area", qop="auth", nonce="8746947a93be8d88219ab22dccc5e3e6", opaque="4334df1313fb0e562393efeaff630d18"
Content-Type: text/html;charset=utf-8
Content-Length: 954
Date: Thu, 13 May 2010 02:22:43 GMT


GET /club-test/IndexServlet HTTP/1.1
Accept: */*
Accept-Language: zh-cn
Accept-Encoding: gzip, deflate
User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; TCO_20100513102058; .NET CLR 2.0.50727; .NET CLR 3.0.04506.30; CIBA; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729)
Host: 127.0.0.1:8080
Connection: Keep-Alive
Authorization: Digest username="tomcat", realm="Basic Authentication Area", qop="auth", algorithm="MD5", uri="/club-test/IndexServlet", nonce="8746947a93be8d88219ab22dccc5e3e6", nc=00000001, cnonce="63594dae28ab96e3bd3fc7e3fabca0d8", opaque="4334df1313fb0e562393efeaff630d18", response="627bf900cec889712184f0e21fcef80e"


HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Pragma: No-cache
Cache-Control: no-cache
Expires: Thu, 01 Jan 1970 08:00:00 CST
Set-Cookie: JSESSIONID=B235CF234E263363B7F46DC4DF6D23BD; Path=/club-test
Transfer-Encoding: chunked
Date: Thu, 13 May 2010 02:22:57 GMT

GET /club-test/Music HTTP/1.1
Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/xaml+xml, application/vnd.ms-xpsdocument, application/x-ms-xbap, application/x-ms-application, application/x-shockwave-flash, application/vnd.ms-excel, application/vnd.ms-powerpoint, application/msword, */*
Accept-Language: zh-cn
Accept-Encoding: gzip, deflate
User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; TCO_20100513102058; .NET CLR 2.0.50727; .NET CLR 3.0.04506.30; CIBA; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729)
Host: 127.0.0.1:8080
Connection: Keep-Alive
Cookie: JSESSIONID=B235CF234E263363B7F46DC4DF6D23BD
Authorization: Digest username="tomcat", realm="Basic Authentication Area", qop="auth", algorithm="MD5", uri="/club-test/Music", nonce="8746947a93be8d88219ab22dccc5e3e6", nc=00000002, cnonce="cd29e5745208fc6b4e7d0b86770c81ca", response="bb1c27f7a0f955967541c46090add6e8"
该模块目前仅支持WS-UsernameToken认证,现在我要新增digest认证,目前已做的修改如下(onvif_proc_data_srv函数只修改了一半),请你补全剩下的修改(包括onvif_proc_data_srv函数以及其他需要修改的地方) #### 流程梳理 - 初始化认证状态:`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 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生成及过期管理: ```c #include <openssl/rand.h> #include <openssl/err.h> #include <time.h> #include <string.h> #define ERROR_CRYPTO_FAIL -1 #define ERROR_BUFFER_OVERFLOW -2 #define OK 0 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->digest_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) { /* 现有代码... */ /* 在解析SOAP之前添加Digest认证检查 */ context->digest_authenticated = FALSE; /* 检查是否有Digest认证头 */ if (context->digest_username[0] != '\0') { /* 执行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; } } /* 检查是否至少有一个认证通过 */ if (!context->digest_authenticated && !soap->wss_authenticated) { /* 没有提供任何认证凭据,返回401 */ context->code = HTTP_UNAUTHORIZED; return ERROR; } /* 继续原有处理逻辑... */ ``` #### 步骤六:实现Digest认证验证函数 实现一个函数,用于验证Digest认证的响应是否正确。 ```c 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 *)TP_DEFAULT_PSWD, (U32)strlen(TP_DEFAULT_PSWD)); 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; /* 认证失败 */ } #include <openssl/md5.h> /* 计算字符串的MD5哈希值(十六进制格式) */ void tpssl_md5_hex(const char *input, char *output) { if (!input || !output) return; unsigned char digest[MD5_DIGEST_LENGTH]; MD5_CTX context; MD5_Init(&context); MD5_Update(&context, input, strlen(input)); MD5_Final(digest, &context); for (int i = 0; i < MD5_DIGEST_LENGTH; i++) { sprintf(output + (i * 2), "%02x", digest[i]); } output[32] = '\0'; /* MD5总是32字符 */ } /* 将HTTP方法枚举转换为字符串 */ const char *http_method_str(HTTP_METHOD method) { switch(method) { case HTTP_GET: return "GET"; case HTTP_POST: return "POST"; case HTTP_PUT: return "PUT"; case HTTP_DELETE: return "DELETE"; default: return "UNKNOWN"; } } ```
09-13
该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"; } } ```
最新发布
09-16
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值