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
14use std::path::PathBuf;
15use std::sync::OnceLock;
16
17use jsonwebtoken::{DecodingKey, EncodingKey};
18use rsa::pkcs1::{EncodeRsaPrivateKey, EncodeRsaPublicKey};
19use thiserror::Error;
20
21use crate::keys::{KeyError, RsaSigningKey};
22
23#[derive(Debug, Error)]
24pub enum TokenAuthorityError {
25    #[error("signing_key_path is not configured")]
26    PathMissing,
27
28    #[error("signing key file not found at {0}")]
29    FileMissing(PathBuf),
30
31    #[error("config unavailable: {0}")]
32    Config(String),
33
34    #[error("signing key secret unavailable: {0}")]
35    Secret(String),
36
37    #[error("key load failed: {0}")]
38    Key(#[from] KeyError),
39
40    #[error("jwt key conversion failed: {0}")]
41    KeyConvert(#[source] jsonwebtoken::errors::Error),
42
43    #[error("RSA DER encoding failed: {0}")]
44    Pkcs1Encode(#[source] rsa::pkcs1::Error),
45}
46
47pub type TokenAuthorityResult<T> = Result<T, TokenAuthorityError>;
48
49#[expect(
50    clippy::struct_field_names,
51    reason = "all fields are RSA-derived keys; the `_key` suffix distinguishes their format"
52)]
53pub(crate) struct Authority {
54    signing_key: RsaSigningKey,
55    encoding_key: EncodingKey,
56    decoding_key: DecodingKey,
57}
58
59static CELL: OnceLock<Authority> = OnceLock::new();
60
61pub fn init() -> TokenAuthorityResult<()> {
62    if CELL.get().is_some() {
63        return Ok(());
64    }
65    let authority = load_from_secret_or_file()?;
66    drop(CELL.set(authority));
67    Ok(())
68}
69
70fn load_from_secret_or_file() -> TokenAuthorityResult<Authority> {
71    if let Some(pem) = systemprompt_config::SecretsBootstrap::signing_key_pem()
72        .map_err(|e| TokenAuthorityError::Secret(e.to_string()))?
73    {
74        let signing_key = RsaSigningKey::from_pkcs8_pem(&pem)?;
75        return build(signing_key);
76    }
77    load()
78}
79
80fn load() -> TokenAuthorityResult<Authority> {
81    let config = systemprompt_models::Config::get()
82        .map_err(|e| TokenAuthorityError::Config(e.to_string()))?;
83    let path = &config.signing_key_path;
84    if path.as_os_str().is_empty() {
85        return Err(TokenAuthorityError::PathMissing);
86    }
87    if !path.exists() {
88        return Err(TokenAuthorityError::FileMissing(path.clone()));
89    }
90    let signing_key = RsaSigningKey::load_from_pem_file(path)?;
91    build(signing_key)
92}
93
94pub(crate) fn build(signing_key: RsaSigningKey) -> TokenAuthorityResult<Authority> {
95    let der = signing_key
96        .private_key()
97        .to_pkcs1_der()
98        .map_err(TokenAuthorityError::Pkcs1Encode)?;
99    let encoding_key = EncodingKey::from_rsa_der(der.as_bytes());
100    let pub_der = signing_key
101        .public_key()
102        .to_pkcs1_der()
103        .map_err(TokenAuthorityError::Pkcs1Encode)?;
104    let decoding_key = DecodingKey::from_rsa_der(pub_der.as_bytes());
105    Ok(Authority {
106        signing_key,
107        encoding_key,
108        decoding_key,
109    })
110}
111
112fn authority() -> TokenAuthorityResult<&'static Authority> {
113    if let Some(a) = CELL.get() {
114        return Ok(a);
115    }
116    let a = load_from_secret_or_file()?;
117    drop(CELL.set(a));
118    CELL.get().ok_or(TokenAuthorityError::PathMissing)
119}
120
121pub fn signing_key() -> TokenAuthorityResult<&'static RsaSigningKey> {
122    Ok(&authority()?.signing_key)
123}
124
125pub fn encoding_key() -> TokenAuthorityResult<&'static EncodingKey> {
126    Ok(&authority()?.encoding_key)
127}
128
129pub fn active_kid() -> TokenAuthorityResult<&'static str> {
130    Ok(authority()?.signing_key.kid())
131}
132
133pub fn decoding_key_for_kid(kid: &str) -> TokenAuthorityResult<Option<&'static DecodingKey>> {
134    let a = authority()?;
135    if a.signing_key.kid() == kid {
136        Ok(Some(&a.decoding_key))
137    } else {
138        Ok(None)
139    }
140}
141
142pub fn decoding_key() -> TokenAuthorityResult<&'static DecodingKey> {
143    Ok(&authority()?.decoding_key)
144}
145
146#[doc(hidden)]
147pub fn install_for_test(key: RsaSigningKey) {
148    if CELL.get().is_some() {
149        return;
150    }
151    if let Ok(a) = build(key) {
152        drop(CELL.set(a));
153    }
154}