Skip to main content

cc_teec/teec/
ca_auth.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (C) 2025-2026 KylinSoft Co., Ltd. <https://www.kylinos.cn/>
3// See LICENSES for license details.
4
5//! TEE CA 认证模块
6//!
7//! 提供 CA(Client Application)身份认证功能,用于 TA 的 ACL 访问控制。
8//!
9//! ## 主要功能
10//!
11//! - **CA 身份标识**: 基于调用者路径生成 UUID v5
12//! - **签名验证**: 使用 tasign 验证 ELF 文件签名
13//! - **结果缓存**: 基于 (PID, inode, mtime, dev) 的复合键缓存验签结果
14//!
15//! ## CA 验签缓存
16//!
17//! 使用 DashMap 实现无锁并发访问,缓存键为 (进程ID, CA文件标识)。
18//!
19//! CA 文件标识基于 inode、mtime 和 dev,可检测:
20//! - 文件被替换(inode 变化)
21//! - 文件内容修改(mtime 变化)
22//! - 不同设备上的同名文件(dev 区分)
23//!
24//! 缓存策略:
25//! - 同一进程内,CA 文件未改变时复用验签结果
26//! - 进程 fork 后,若执行文件被替换则重新验签
27//! - 不同进程的 CA 文件各自独立缓存
28//!
29//! ## TA 访问控制
30//!
31//! TA 使用 `CaAuthInfo` 进行 ACL 决策,只关心:
32//! - `ca_uuid`: 哪个 CA 发起的请求
33//! - `verified`: 验签是否通过(包含签名验证和证书链验证)
34
35use std::{fs, os::unix::fs::MetadataExt, sync::LazyLock};
36
37use dashmap::DashMap;
38use log::{debug, warn};
39
40use teec_protocol::{CaAuthInfo, path_to_uuid};
41
42/// 生产环境 TA 签名根证书。
43/// 默认启用;当 `test-root-ca` feature 启用时改用 tasign 内置测试根证书。
44#[cfg(not(feature = "test-root-ca"))]
45const TA_SIGN_ROOT_CA_PEM: &[u8] = include_bytes!("../../certs/kylin-xtee-ca-sign-root.pem");
46
47/// 加载 CA 根证书 PEM 字节,用于证书链验证。
48///
49/// 编译期通过 feature flag 决定使用哪个根证书:
50/// - 默认(未启用 `test-root-ca`):嵌入的生产根证书(`certs/kylin-xtee-ca-sign-root.pem`);
51/// - 启用 `test-root-ca`:tasign 内置测试根证书(`tasign::cert::CA_CERT_PEM`),
52///   以支持 TEST 签名 ELF 的完整证书链验证。
53fn load_root_ca_cert() -> &'static [u8] {
54    #[cfg(not(feature = "test-root-ca"))]
55    {
56        debug!("使用内置生产 CA 根证书");
57        return TA_SIGN_ROOT_CA_PEM;
58    }
59
60    #[cfg(feature = "test-root-ca")]
61    {
62        debug!("使用 tasign 内置测试根证书");
63        return tasign::cert::CA_CERT_PEM;
64    }
65}
66
67/// 缓存键:(进程ID, CA文件标识)
68#[derive(Debug, Clone, Hash, PartialEq, Eq)]
69struct CacheKey {
70    pid: i32,
71    ca_file_id: CaFileId,
72}
73
74/// CA 文件唯一标识(基于 inode + mtime + dev)
75#[derive(Debug, Clone, Hash, PartialEq, Eq)]
76struct CaFileId {
77    inode: u64,      // inode 号
78    mtime: i64,      // 修改时间(秒)
79    mtime_nsec: i64, // 修改时间(纳秒部分)
80    dev: u64,        // 设备号
81}
82
83/// 获取 CA 文件的唯一标识(读取文件元数据)
84fn get_ca_file_id(path: &str) -> Option<CaFileId> {
85    match fs::metadata(path) {
86        Ok(metadata) => Some(CaFileId {
87            inode: metadata.ino(),
88            mtime: metadata.mtime(),
89            mtime_nsec: metadata.mtime_nsec(),
90            dev: metadata.dev(),
91        }),
92        Err(_) => None,
93    }
94}
95
96/// 全局缓存:使用 DashMap 提供无锁并发访问
97/// key为(PID, CA文件标识),value为CA认证信息
98static CA_AUTH_CACHE: LazyLock<DashMap<CacheKey, CaAuthInfo>> = LazyLock::new(DashMap::new);
99
100/// 获取或执行 CA 认证(带缓存)
101///
102/// 返回 CA 认证信息,用于 TA 的 ACL 访问控制。
103///
104/// 缓存键基于进程 ID 和 CA 文件标识,可检测:
105/// - 同一进程内文件未改变时复用结果
106/// - 文件被替换(inode/mtime 变化)时重新验签
107/// - 不同进程的 CA 文件各自独立缓存
108///
109/// # 返回
110///
111/// 返回 `CaAuthInfo`,包含:
112/// - `ca_uuid`: CA 的唯一标识(基于调用者路径生成的 UUID v5)
113/// - `verified`: 验签是否通过(包含签名验证和证书链验证)
114pub fn get_or_verify_ca() -> CaAuthInfo {
115    // 获取当前进程 ID
116    let pid = std::process::id() as i32;
117
118    // 获取调用者可执行文件路径
119    let ca_path = match std::fs::read_link("/proc/self/exe") {
120        Ok(path) => path.to_string_lossy().to_string(),
121        Err(_) => "<unknown>".to_string(),
122    };
123
124    // 生成 CA UUID(基于路径)
125    let ca_uuid = path_to_uuid(&ca_path);
126
127    // 获取 CA 文件标识
128    let ca_file_id = match get_ca_file_id(&ca_path) {
129        Some(id) => id,
130        None => {
131            // 无法获取文件元数据,跳过缓存直接验签
132            warn!("无法获取CA文件元数据: {}", ca_path);
133            return perform_ca_auth_internal(ca_uuid, &ca_path);
134        }
135    };
136
137    // 构建缓存键
138    let key = CacheKey { pid, ca_file_id };
139
140    if let Some(result) = CA_AUTH_CACHE.get(&key) {
141        debug!(
142            "CA认证缓存命中: pid={}, inode={}",
143            pid, key.ca_file_id.inode
144        );
145        return result.value().clone();
146    }
147
148    // 缓存未命中,执行验签
149    debug!("CA认证缓存未命中,执行验签: pid={}, path={}", pid, ca_path);
150    let result = perform_ca_auth_internal(ca_uuid, &ca_path);
151
152    CA_AUTH_CACHE.insert(key, result.clone());
153
154    result
155}
156
157/// 清除所有缓存(主要用于测试)
158pub fn clear_cache() {
159    CA_AUTH_CACHE.clear();
160}
161
162/// 内部认证函数:调用 tasign 库进行签名验证,生成 CaAuthInfo
163fn perform_ca_auth_internal(ca_uuid: String, ca_path: &str) -> CaAuthInfo {
164    if ca_path == "<unknown>" {
165        warn!("CA path unknown, skipping verification");
166        return CaAuthInfo {
167            ca_uuid,
168            verified: false,
169        };
170    }
171
172    debug!("开始验证 CA ELF 签名: {}", ca_path);
173
174    let elf_data = match fs::read(ca_path) {
175        Ok(data) => data,
176        Err(e) => {
177            warn!("无法读取ELF文件: {}", e);
178            return CaAuthInfo {
179                ca_uuid,
180                verified: false,
181            };
182        }
183    };
184
185    let ca_pem = load_root_ca_cert();
186    debug!("CA 根证书已加载(编译期嵌入)");
187
188    let verified = match tasign::verify_elf_signature(&elf_data, Some(ca_pem)) {
189        Ok(_) => {
190            debug!("CA ELF 签名验证成功(含证书链验证)");
191            true
192        }
193        Err(e) => {
194            warn!("签名验证失败: {}", e);
195            false
196        }
197    };
198
199    CaAuthInfo { ca_uuid, verified }
200}
201
202#[cfg(test)]
203mod tests {
204    use super::*;
205
206    // CA 验签缓存测试
207
208    #[test]
209    fn test_ca_file_id_uniqueness() {
210        // 测试 CaFileId 的比较逻辑
211        let id1 = CaFileId {
212            inode: 12345,
213            mtime: 1000,
214            mtime_nsec: 0,
215            dev: 1,
216        };
217
218        let id2 = CaFileId {
219            inode: 12345,
220            mtime: 1000,
221            mtime_nsec: 0,
222            dev: 1,
223        };
224
225        let id3 = CaFileId {
226            inode: 12345,
227            mtime: 2000, // mtime不同
228            mtime_nsec: 0,
229            dev: 1,
230        };
231
232        assert_eq!(id1, id2);
233        assert_ne!(id1, id3);
234    }
235
236    #[test]
237    fn test_clear_cache() {
238        let test_key = CacheKey {
239            pid: 99999,
240            ca_file_id: CaFileId {
241                inode: 12345,
242                mtime: 1000,
243                mtime_nsec: 0,
244                dev: 67890,
245            },
246        };
247
248        let test_info = CaAuthInfo {
249            ca_uuid: "test-uuid".to_string(),
250            verified: true,
251        };
252
253        CA_AUTH_CACHE.insert(test_key.clone(), test_info);
254
255        clear_cache();
256        assert!(
257            !CA_AUTH_CACHE.contains_key(&test_key),
258            "cache should be cleared"
259        );
260    }
261
262    #[test]
263    fn test_get_or_verify_ca_returns_info() {
264        clear_cache();
265        let info = get_or_verify_ca();
266        assert!(!info.ca_uuid.is_empty());
267    }
268
269    #[test]
270    fn test_get_or_verify_ca_caches_result() {
271        clear_cache();
272        let info1 = get_or_verify_ca();
273        let info2 = get_or_verify_ca();
274        assert_eq!(info1.ca_uuid, info2.ca_uuid);
275        assert_eq!(info1.verified, info2.verified);
276        clear_cache();
277    }
278
279    #[test]
280    fn test_perform_ca_auth_internal_unknown_path() {
281        let result = perform_ca_auth_internal("test-uuid".to_string(), "<unknown>");
282        assert_eq!(result.ca_uuid, "test-uuid");
283        assert!(!result.verified);
284    }
285
286    #[test]
287    fn test_perform_ca_auth_internal_nonexistent_path() {
288        let result =
289            perform_ca_auth_internal("test-uuid".to_string(), "/nonexistent/path/to/binary");
290        assert_eq!(result.ca_uuid, "test-uuid");
291        assert!(!result.verified);
292    }
293
294    #[test]
295    fn test_perform_ca_auth_internal_current_exe() {
296        let exe_path = std::fs::read_link("/proc/self/exe")
297            .unwrap()
298            .to_string_lossy()
299            .to_string();
300        let result = perform_ca_auth_internal("test-uuid".to_string(), &exe_path);
301        assert_eq!(result.ca_uuid, "test-uuid");
302    }
303
304    #[test]
305    fn test_get_ca_file_id_nonexistent() {
306        assert!(get_ca_file_id("/nonexistent/file").is_none());
307    }
308
309    #[test]
310    fn test_get_ca_file_id_current_exe() {
311        let exe_path = std::fs::read_link("/proc/self/exe")
312            .unwrap()
313            .to_string_lossy()
314            .to_string();
315        let file_id = get_ca_file_id(&exe_path);
316        assert!(file_id.is_some());
317        let id = file_id.unwrap();
318        assert!(id.inode > 0);
319    }
320}