Skip to main content

systemprompt_security/keys/
authority.rs

1//! Process-wide RS256 signing-key authority.
2//!
3//! Loads the RSA private key once and caches it in a `OnceLock`. The key is
4//! resolved from the `signing_key_pem` secret first (the cloud path, where the
5//! filesystem is read-only and the PEM is injected as an env secret), falling
6//! back to the file at `signing_key_path` (the local-dev path). [`init`] is
7//! called at bootstrap so a missing key fails startup rather than the first
8//! request; the lazy first-use fallback uses the same resolution.
9//! Token-minting paths use [`encoding_key`] / [`active_kid`] to produce RS256
10//! JWTs whose `kid` matches the JWKS this deployment publishes at
11//! `/.well-known/jwks.json`. Token-verifying paths use [`decoding_key_for_kid`]
12//! to look up the public half by `kid`.
13//!
14//! Copyright (c) systemprompt.io — Business Source License 1.1.
15//! See <https://systemprompt.io> for licensing details.
16
17use std::path::PathBuf;
18use std::sync::OnceLock;
19
20use jsonwebtoken::{DecodingKey, EncodingKey};
21use rsa::pkcs1::{EncodeRsaPrivateKey, EncodeRsaPublicKey};
22use thiserror::Error;
23
24use crate::keys::{KeyError, RsaSigningKey};
25
26#[derive(Debug, Error)]
27pub enum TokenAuthorityError {
28    #[error("signing_key_path is not configured")]
29    PathMissing,
30
31    #[error("signing key file not found at {0}")]
32    FileMissing(PathBuf),
33
34    #[error("config unavailable: {0}")]
35    Config(String),
36
37    #[error("signing key secret unavailable: {0}")]
38    Secret(String),
39
40    #[error("key load failed: {0}")]
41    Key(#[from] KeyError),
42
43    #[error("jwt key conversion failed: {0}")]
44    KeyConvert(#[source] jsonwebtoken::errors::Error),
45
46    #[error("RSA DER encoding failed: {0}")]
47    Pkcs1Encode(#[source] rsa::pkcs1::Error),
48}
49
50pub type TokenAuthorityResult<T> = Result<T, TokenAuthorityError>;
51
52#[expect(
53    clippy::struct_field_names,
54    reason = "all fields are RSA-derived keys; the `_key` suffix distinguishes their format"
55)]
56pub(crate) struct Authority {
57    signing_key: RsaSigningKey,
58    encoding_key: EncodingKey,
59    decoding_key: DecodingKey,
60}
61
62static CELL: OnceLock<Authority> = OnceLock::new();
63
64pub fn init() -> TokenAuthorityResult<()> {
65    if CELL.get().is_some() {
66        return Ok(());
67    }
68    let authority = load_from_secret_or_file()?;
69    drop(CELL.set(authority));
70    Ok(())
71}
72
73fn load_from_secret_or_file() -> TokenAuthorityResult<Authority> {
74    if let Some(pem) = systemprompt_config::SecretsBootstrap::signing_key_pem()
75        .map_err(|e| TokenAuthorityError::Secret(e.to_string()))?
76    {
77        let signing_key = RsaSigningKey::from_pkcs8_pem(&pem)?;
78        return build(signing_key);
79    }
80    load()
81}
82
83fn load() -> TokenAuthorityResult<Authority> {
84    let config = systemprompt_models::Config::get()
85        .map_err(|e| TokenAuthorityError::Config(e.to_string()))?;
86    let path = &config.signing_key_path;
87    if path.as_os_str().is_empty() {
88        return Err(TokenAuthorityError::PathMissing);
89    }
90    if !path.exists() {
91        return Err(TokenAuthorityError::FileMissing(path.clone()));
92    }
93    let signing_key = RsaSigningKey::load_from_pem_file(path)?;
94    build(signing_key)
95}
96
97pub(crate) fn build(signing_key: RsaSigningKey) -> TokenAuthorityResult<Authority> {
98    let der = signing_key
99        .private_key()
100        .to_pkcs1_der()
101        .map_err(TokenAuthorityError::Pkcs1Encode)?;
102    let encoding_key = EncodingKey::from_rsa_der(der.as_bytes());
103    let pub_der = signing_key
104        .public_key()
105        .to_pkcs1_der()
106        .map_err(TokenAuthorityError::Pkcs1Encode)?;
107    let decoding_key = DecodingKey::from_rsa_der(pub_der.as_bytes());
108    Ok(Authority {
109        signing_key,
110        encoding_key,
111        decoding_key,
112    })
113}
114
115fn authority() -> TokenAuthorityResult<&'static Authority> {
116    if let Some(a) = CELL.get() {
117        return Ok(a);
118    }
119    let a = load_from_secret_or_file()?;
120    drop(CELL.set(a));
121    CELL.get().ok_or(TokenAuthorityError::PathMissing)
122}
123
124pub fn signing_key() -> TokenAuthorityResult<&'static RsaSigningKey> {
125    Ok(&authority()?.signing_key)
126}
127
128pub fn encoding_key() -> TokenAuthorityResult<&'static EncodingKey> {
129    Ok(&authority()?.encoding_key)
130}
131
132pub fn active_kid() -> TokenAuthorityResult<&'static str> {
133    Ok(authority()?.signing_key.kid())
134}
135
136pub fn decoding_key_for_kid(kid: &str) -> TokenAuthorityResult<Option<&'static DecodingKey>> {
137    let a = authority()?;
138    if a.signing_key.kid() == kid {
139        Ok(Some(&a.decoding_key))
140    } else {
141        Ok(None)
142    }
143}
144
145pub fn decoding_key() -> TokenAuthorityResult<&'static DecodingKey> {
146    Ok(&authority()?.decoding_key)
147}
148
149#[doc(hidden)]
150pub fn install_for_test(key: RsaSigningKey) {
151    if CELL.get().is_some() {
152        return;
153    }
154    if let Ok(a) = build(key) {
155        drop(CELL.set(a));
156    }
157}