systemprompt_models/
licence.rs1use std::collections::BTreeMap;
14
15use chrono::{DateTime, Utc};
16use hmac::{Hmac, KeyInit, Mac};
17use serde::{Deserialize, Serialize};
18use sha2::Sha256;
19use systemprompt_identifiers::{EnterpriseLicenseId, InstanceId, TenantId};
20
21pub const CHECKIN_SIGNATURE_HEADER: &str = "X-Systemprompt-Signature";
22pub const CHECKIN_TENANT_HEADER: &str = "X-Systemprompt-Tenant";
23pub const CHECKIN_SIGNATURE_VERSION: &str = "v1";
24
25type HmacSha256 = Hmac<Sha256>;
26
27#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
29pub struct LicenceCheckin {
30 pub licence_id: Option<EnterpriseLicenseId>,
31 pub tenant_id: Option<TenantId>,
32 pub instance_id: InstanceId,
33 pub core_version: String,
34 pub hosting_provider: String,
35 pub external_host: Option<String>,
36 pub instances_by_role: BTreeMap<String, i64>,
37 pub timestamp: DateTime<Utc>,
38}
39
40#[derive(Debug, Clone, Copy, PartialEq, thiserror::Error)]
42pub enum CheckinSignatureError {
43 #[error("signature header has no `v1=` version prefix")]
44 UnknownVersion,
45 #[error("signature is not valid hex: {0}")]
46 Hex(#[source] hex::FromHexError),
47 #[error("signature does not match the body")]
48 Mismatch(#[source] hmac::digest::MacError),
49}
50
51fn mac(secret: &[u8]) -> HmacSha256 {
52 #[expect(clippy::expect_used, reason = "HMAC-SHA256 accepts any key length")]
53 <HmacSha256 as KeyInit>::new_from_slice(secret).expect("HMAC accepts any key length")
54}
55
56#[must_use]
57pub fn sign_checkin(body: &[u8], secret: &[u8]) -> String {
58 let mut mac = mac(secret);
59 mac.update(body);
60 format!(
61 "{CHECKIN_SIGNATURE_VERSION}={}",
62 hex::encode(mac.finalize().into_bytes())
63 )
64}
65
66pub fn verify_checkin(
67 body: &[u8],
68 header_value: &str,
69 secret: &[u8],
70) -> Result<(), CheckinSignatureError> {
71 let digest = header_value
72 .strip_prefix(CHECKIN_SIGNATURE_VERSION)
73 .and_then(|rest| rest.strip_prefix('='))
74 .ok_or(CheckinSignatureError::UnknownVersion)?;
75 let expected = hex::decode(digest).map_err(CheckinSignatureError::Hex)?;
76 let mut mac = mac(secret);
77 mac.update(body);
78 mac.verify_slice(&expected)
79 .map_err(CheckinSignatureError::Mismatch)
80}