Skip to main content

systemprompt_security/keys/
authority.rs

1//! Process-wide RS256 signing-key authority.
2//!
3//! Loads the configured RSA private key once and caches it in a
4//! `OnceLock`. Token-minting paths use [`encoding_key`] / [`active_kid`]
5//! to produce RS256 JWTs whose `kid` matches the JWKS this deployment
6//! publishes at `/.well-known/jwks.json`. Token-verifying paths use
7//! [`decoding_key_for_kid`] to look up the public half by `kid`.
8
9use std::path::PathBuf;
10use std::sync::OnceLock;
11
12use jsonwebtoken::{DecodingKey, EncodingKey};
13use rsa::pkcs1::{EncodeRsaPrivateKey, EncodeRsaPublicKey};
14use thiserror::Error;
15
16use crate::keys::{KeyError, RsaSigningKey};
17
18#[derive(Debug, Error)]
19pub enum TokenAuthorityError {
20    #[error("signing_key_path is not configured")]
21    PathMissing,
22
23    #[error("signing key file not found at {0}")]
24    FileMissing(PathBuf),
25
26    #[error("config unavailable: {0}")]
27    Config(String),
28
29    #[error("key load failed: {0}")]
30    Key(#[from] KeyError),
31
32    #[error("jwt key conversion failed: {0}")]
33    KeyConvert(#[source] jsonwebtoken::errors::Error),
34
35    #[error("RSA DER encoding failed: {0}")]
36    Pkcs1Encode(#[source] rsa::pkcs1::Error),
37}
38
39pub type TokenAuthorityResult<T> = Result<T, TokenAuthorityError>;
40
41#[expect(
42    clippy::struct_field_names,
43    reason = "all fields are RSA-derived keys; the `_key` suffix distinguishes their format"
44)]
45pub(crate) struct Authority {
46    signing_key: RsaSigningKey,
47    encoding_key: EncodingKey,
48    decoding_key: DecodingKey,
49}
50
51static CELL: OnceLock<Authority> = OnceLock::new();
52
53fn load() -> TokenAuthorityResult<Authority> {
54    let config = systemprompt_models::Config::get()
55        .map_err(|e| TokenAuthorityError::Config(e.to_string()))?;
56    let path = &config.signing_key_path;
57    if path.as_os_str().is_empty() {
58        return Err(TokenAuthorityError::PathMissing);
59    }
60    if !path.exists() {
61        return Err(TokenAuthorityError::FileMissing(path.clone()));
62    }
63    let signing_key = RsaSigningKey::load_from_pem_file(path)?;
64    build(signing_key)
65}
66
67pub(crate) fn build(signing_key: RsaSigningKey) -> TokenAuthorityResult<Authority> {
68    let der = signing_key
69        .private_key()
70        .to_pkcs1_der()
71        .map_err(TokenAuthorityError::Pkcs1Encode)?;
72    let encoding_key = EncodingKey::from_rsa_der(der.as_bytes());
73    let pub_der = signing_key
74        .public_key()
75        .to_pkcs1_der()
76        .map_err(TokenAuthorityError::Pkcs1Encode)?;
77    let decoding_key = DecodingKey::from_rsa_der(pub_der.as_bytes());
78    Ok(Authority {
79        signing_key,
80        encoding_key,
81        decoding_key,
82    })
83}
84
85fn authority() -> TokenAuthorityResult<&'static Authority> {
86    if let Some(a) = CELL.get() {
87        return Ok(a);
88    }
89    let a = load()?;
90    drop(CELL.set(a));
91    CELL.get().ok_or(TokenAuthorityError::PathMissing)
92}
93
94pub fn signing_key() -> TokenAuthorityResult<&'static RsaSigningKey> {
95    Ok(&authority()?.signing_key)
96}
97
98pub fn encoding_key() -> TokenAuthorityResult<&'static EncodingKey> {
99    Ok(&authority()?.encoding_key)
100}
101
102pub fn active_kid() -> TokenAuthorityResult<&'static str> {
103    Ok(authority()?.signing_key.kid())
104}
105
106pub fn decoding_key_for_kid(kid: &str) -> TokenAuthorityResult<Option<&'static DecodingKey>> {
107    let a = authority()?;
108    if a.signing_key.kid() == kid {
109        Ok(Some(&a.decoding_key))
110    } else {
111        Ok(None)
112    }
113}
114
115pub fn decoding_key() -> TokenAuthorityResult<&'static DecodingKey> {
116    Ok(&authority()?.decoding_key)
117}
118
119#[doc(hidden)]
120pub fn install_for_test(key: RsaSigningKey) {
121    if CELL.get().is_some() {
122        return;
123    }
124    if let Ok(a) = build(key) {
125        drop(CELL.set(a));
126    }
127}