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