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. [`install`]
9//! is the single write path: it installs the first key and refuses any other,
10//! so a key placed in the cell before bootstrap fails [`init`] instead of
11//! silently replacing the configured one.
12//! Token-minting paths use [`encoding_key`] / [`active_kid`] to produce RS256
13//! JWTs whose `kid` matches the JWKS this deployment publishes at
14//! `/.well-known/jwks.json`. Token-verifying paths use [`decoding_key_for_kid`]
15//! to look up the public half by `kid`.
16//!
17//! Copyright (c) systemprompt.io — Business Source License 1.1.
18//! See <https://systemprompt.io> for licensing details.
19
20use std::path::PathBuf;
21use std::sync::OnceLock;
22
23use jsonwebtoken::{DecodingKey, EncodingKey};
24use rsa::pkcs1::{EncodeRsaPrivateKey, EncodeRsaPublicKey};
25use thiserror::Error;
26
27use crate::keys::{KeyError, RsaSigningKey};
28
29#[derive(Debug, Error)]
30pub enum TokenAuthorityError {
31    #[error("signing_key_path is not configured")]
32    PathMissing,
33
34    #[error("signing key file not found at {0}")]
35    FileMissing(PathBuf),
36
37    #[error("config unavailable: {0}")]
38    Config(#[source] systemprompt_models::errors::GlobalConfigError),
39
40    #[error("signing key secret unavailable: {0}")]
41    Secret(#[source] systemprompt_config::SecretsBootstrapError),
42
43    #[error("key load failed: {0}")]
44    Key(#[from] KeyError),
45
46    #[error("jwt key conversion failed: {0}")]
47    KeyConvert(#[source] jsonwebtoken::errors::Error),
48
49    #[error("RSA DER encoding failed: {0}")]
50    Pkcs1Encode(#[source] rsa::pkcs1::Error),
51
52    #[error("signing key {requested} conflicts with the installed key {installed}")]
53    ConflictingKey {
54        installed: String,
55        requested: String,
56    },
57}
58
59pub type TokenAuthorityResult<T> = Result<T, TokenAuthorityError>;
60
61#[expect(
62    clippy::struct_field_names,
63    reason = "all fields are RSA-derived keys; the `_key` suffix distinguishes their format"
64)]
65pub(crate) struct Authority {
66    signing_key: RsaSigningKey,
67    encoding_key: EncodingKey,
68    decoding_key: DecodingKey,
69}
70
71static CELL: OnceLock<Authority> = OnceLock::new();
72
73pub fn init() -> TokenAuthorityResult<()> {
74    install_authority(load_from_secret_or_file()?)?;
75    Ok(())
76}
77
78pub fn install(signing_key: RsaSigningKey) -> TokenAuthorityResult<()> {
79    install_authority(build(signing_key)?)?;
80    Ok(())
81}
82
83fn install_authority(authority: Authority) -> TokenAuthorityResult<&'static Authority> {
84    let requested = authority.signing_key.kid().to_owned();
85    let installed = CELL.get_or_init(|| authority);
86    if installed.signing_key.kid() == requested {
87        Ok(installed)
88    } else {
89        Err(TokenAuthorityError::ConflictingKey {
90            installed: installed.signing_key.kid().to_owned(),
91            requested,
92        })
93    }
94}
95
96fn load_from_secret_or_file() -> TokenAuthorityResult<Authority> {
97    if let Some(pem) = systemprompt_config::SecretsBootstrap::signing_key_pem()
98        .map_err(TokenAuthorityError::Secret)?
99    {
100        let signing_key = RsaSigningKey::from_pkcs8_pem(&pem)?;
101        return build(signing_key);
102    }
103    load()
104}
105
106fn load() -> TokenAuthorityResult<Authority> {
107    let config = systemprompt_manifest::Config::get().map_err(TokenAuthorityError::Config)?;
108    let path = &config.signing_key_path;
109    if path.as_os_str().is_empty() {
110        return Err(TokenAuthorityError::PathMissing);
111    }
112    if !path.exists() {
113        return Err(TokenAuthorityError::FileMissing(path.clone()));
114    }
115    let signing_key = RsaSigningKey::load_from_pem_file(path)?;
116    build(signing_key)
117}
118
119pub(crate) fn build(signing_key: RsaSigningKey) -> TokenAuthorityResult<Authority> {
120    let der = signing_key
121        .private_key()
122        .to_pkcs1_der()
123        .map_err(TokenAuthorityError::Pkcs1Encode)?;
124    let encoding_key = EncodingKey::from_rsa_der(der.as_bytes());
125    let pub_der = signing_key
126        .public_key()
127        .to_pkcs1_der()
128        .map_err(TokenAuthorityError::Pkcs1Encode)?;
129    let decoding_key = DecodingKey::from_rsa_der(pub_der.as_bytes());
130    Ok(Authority {
131        signing_key,
132        encoding_key,
133        decoding_key,
134    })
135}
136
137fn authority() -> TokenAuthorityResult<&'static Authority> {
138    if let Some(a) = CELL.get() {
139        return Ok(a);
140    }
141    install_authority(load_from_secret_or_file()?)
142}
143
144pub fn signing_key() -> TokenAuthorityResult<&'static RsaSigningKey> {
145    Ok(&authority()?.signing_key)
146}
147
148pub fn encoding_key() -> TokenAuthorityResult<&'static EncodingKey> {
149    Ok(&authority()?.encoding_key)
150}
151
152pub fn active_kid() -> TokenAuthorityResult<&'static str> {
153    Ok(authority()?.signing_key.kid())
154}
155
156pub fn decoding_key_for_kid(kid: &str) -> TokenAuthorityResult<Option<&'static DecodingKey>> {
157    let a = authority()?;
158    if a.signing_key.kid() == kid {
159        Ok(Some(&a.decoding_key))
160    } else {
161        Ok(None)
162    }
163}
164
165pub fn decoding_key() -> TokenAuthorityResult<&'static DecodingKey> {
166    Ok(&authority()?.decoding_key)
167}