Skip to main content

systemprompt_users/services/
device_cert_service.rs

1//! Device-certificate fingerprint registration and lookup.
2//!
3//! Copyright (c) systemprompt.io — Business Source License 1.1.
4//! See <https://systemprompt.io> for licensing details.
5
6use std::sync::Arc;
7use systemprompt_identifiers::{DeviceCertId, UserId};
8
9use crate::error::{Result, UserError};
10use crate::models::UserDeviceCert;
11use crate::repository::{EnrollDeviceCertParams, UserRepository};
12
13const FINGERPRINT_LEN: usize = 64;
14
15#[derive(Debug, Clone)]
16pub struct EnrollParams<'a> {
17    pub user_id: &'a UserId,
18    pub fingerprint: &'a str,
19    pub label: &'a str,
20}
21
22#[derive(Debug, Clone)]
23pub struct DeviceCertService {
24    repository: Arc<UserRepository>,
25}
26
27impl DeviceCertService {
28    pub const fn new(repository: Arc<UserRepository>) -> Self {
29        Self { repository }
30    }
31
32    pub async fn enroll(&self, params: EnrollParams<'_>) -> Result<UserDeviceCert> {
33        let label = params.label.trim();
34        if label.is_empty() {
35            return Err(UserError::Validation(
36                "device cert label must not be empty".into(),
37            ));
38        }
39        let fingerprint = normalize_fingerprint(params.fingerprint)?;
40        let id = DeviceCertId::generate();
41        self.repository
42            .enroll_device_cert(EnrollDeviceCertParams {
43                id: &id,
44                user_id: params.user_id,
45                fingerprint: &fingerprint,
46                label,
47            })
48            .await
49    }
50
51    pub async fn verify(&self, fingerprint: &str) -> Result<Option<UserDeviceCert>> {
52        let normalized = normalize_fingerprint(fingerprint)?;
53        self.repository
54            .find_active_device_cert_by_fingerprint(&normalized)
55            .await
56    }
57
58    pub async fn list_for_user(&self, user_id: &UserId) -> Result<Vec<UserDeviceCert>> {
59        self.repository.list_device_certs_for_user(user_id).await
60    }
61
62    pub async fn revoke(&self, id: &DeviceCertId, user_id: &UserId) -> Result<bool> {
63        self.repository.revoke_device_cert(id, user_id).await
64    }
65}
66
67fn normalize_fingerprint(fingerprint: &str) -> Result<String> {
68    let trimmed = fingerprint.trim().to_ascii_lowercase();
69    if trimmed.len() != FINGERPRINT_LEN {
70        return Err(UserError::Validation(format!(
71            "device cert fingerprint must be {FINGERPRINT_LEN} hex chars (SHA-256)",
72        )));
73    }
74    if !trimmed.bytes().all(|b| b.is_ascii_hexdigit()) {
75        return Err(UserError::Validation(
76            "device cert fingerprint must be hex".into(),
77        ));
78    }
79    Ok(trimmed)
80}