Skip to main content

systemprompt_models/
licence.rs

1//! The licence check-in wire contract shared by the reporting job and the
2//! control-plane receiver.
3//!
4//! A check-in is a JSON [`LicenceCheckin`] body sent by `POST` with
5//! [`CHECKIN_SIGNATURE_HEADER`] set to `v1=<hex>`, where `<hex>` is the
6//! lowercase hex HMAC-SHA256 of the exact body bytes keyed by the raw bytes
7//! of the shared signing secret. [`CHECKIN_TENANT_HEADER`] carries the tenant
8//! id when one is configured.
9//!
10//! Copyright (c) systemprompt.io — Business Source License 1.1.
11//! See <https://systemprompt.io> for licensing details.
12
13use 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/// The signed body of one licence check-in.
28#[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/// Why a check-in signature header was rejected.
41#[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}