Skip to main content

systemprompt_security/keys/
mod.rs

1//! RSA signing-key infrastructure for systemprompt.io's federated JWT plane.
2//!
3//! Provides an [`RsaSigningKey`] wrapper around an `rsa::RsaPrivateKey` that
4//! can be generated, loaded from PKCS#8 PEM, persisted to PEM, and exposes a
5//! deterministic `kid` (SHA-256 of the DER-encoded `SubjectPublicKeyInfo`,
6//! base64 URL-encoded, no padding). The accompanying [`jwks`] module turns the
7//! public half into a JWKS document.
8//!
9//! Copyright (c) systemprompt.io — Business Source License 1.1.
10//! See <https://systemprompt.io> for licensing details.
11
12use std::fs;
13use std::path::Path;
14
15use base64::Engine;
16use base64::engine::general_purpose::URL_SAFE_NO_PAD;
17use pkcs8::LineEnding;
18use rsa::pkcs8::{DecodePrivateKey, EncodePrivateKey, EncodePublicKey};
19use rsa::rand_core::OsRng;
20use rsa::{RsaPrivateKey, RsaPublicKey};
21use sha2::{Digest, Sha256};
22
23pub mod authority;
24pub mod jwks;
25pub mod jwks_client;
26
27pub use authority::{TokenAuthorityError, TokenAuthorityResult};
28pub use jwks::{Jwk, Jwks};
29pub use jwks_client::{JwksClient, JwksClientError};
30
31pub const DEFAULT_RSA_BITS: usize = 2048;
32
33#[derive(Debug, thiserror::Error)]
34pub enum KeyError {
35    #[error("RSA key generation failed: {0}")]
36    Generation(#[source] rsa::Error),
37    #[error("PKCS#8 encoding failed: {0}")]
38    Encode(#[source] pkcs8::Error),
39    #[error("SPKI encoding failed: {0}")]
40    EncodeSpki(#[source] pkcs8::spki::Error),
41    #[error("PKCS#8 decoding failed: {0}")]
42    Decode(#[source] pkcs8::Error),
43    #[error("I/O error for {path}: {source}")]
44    Io {
45        path: String,
46        #[source]
47        source: std::io::Error,
48    },
49}
50
51#[derive(Clone)]
52pub struct RsaSigningKey {
53    private_key: RsaPrivateKey,
54    public_key: RsaPublicKey,
55    kid: String,
56}
57
58impl std::fmt::Debug for RsaSigningKey {
59    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
60        f.debug_struct("RsaSigningKey")
61            .field("kid", &self.kid)
62            .finish_non_exhaustive()
63    }
64}
65
66impl RsaSigningKey {
67    pub fn generate() -> Result<Self, KeyError> {
68        Self::generate_bits(DEFAULT_RSA_BITS)
69    }
70
71    pub fn generate_bits(bits: usize) -> Result<Self, KeyError> {
72        let mut rng = OsRng;
73        let private_key = RsaPrivateKey::new(&mut rng, bits).map_err(KeyError::Generation)?;
74        Self::from_private(private_key)
75    }
76
77    pub fn from_pkcs8_pem(pem: &str) -> Result<Self, KeyError> {
78        let private_key = RsaPrivateKey::from_pkcs8_pem(pem).map_err(KeyError::Decode)?;
79        Self::from_private(private_key)
80    }
81
82    pub fn load_from_pem_file(path: &Path) -> Result<Self, KeyError> {
83        let pem = fs::read_to_string(path).map_err(|source| KeyError::Io {
84            path: path.display().to_string(),
85            source,
86        })?;
87        Self::from_pkcs8_pem(&pem)
88    }
89
90    pub fn to_pkcs8_pem(&self) -> Result<String, KeyError> {
91        self.private_key
92            .to_pkcs8_pem(LineEnding::LF)
93            .map(|s| s.to_string())
94            .map_err(KeyError::Encode)
95    }
96
97    pub fn write_pem_file(&self, path: &Path) -> Result<(), KeyError> {
98        let pem = self.to_pkcs8_pem()?;
99        fs::write(path, pem).map_err(|source| KeyError::Io {
100            path: path.display().to_string(),
101            source,
102        })
103    }
104
105    pub const fn public_key(&self) -> &RsaPublicKey {
106        &self.public_key
107    }
108
109    pub const fn private_key(&self) -> &RsaPrivateKey {
110        &self.private_key
111    }
112
113    pub fn kid(&self) -> &str {
114        &self.kid
115    }
116
117    pub fn jwk(&self) -> Jwk {
118        Jwk::from_rsa_public_key(&self.public_key, self.kid.clone())
119    }
120
121    pub fn jwks(&self) -> Jwks {
122        Jwks {
123            keys: vec![self.jwk()],
124        }
125    }
126
127    fn from_private(private_key: RsaPrivateKey) -> Result<Self, KeyError> {
128        let public_key = RsaPublicKey::from(&private_key);
129        let kid = compute_kid(&public_key)?;
130        Ok(Self {
131            private_key,
132            public_key,
133            kid,
134        })
135    }
136}
137
138pub fn compute_kid(public_key: &RsaPublicKey) -> Result<String, KeyError> {
139    let der = public_key
140        .to_public_key_der()
141        .map_err(KeyError::EncodeSpki)?;
142    let digest = Sha256::digest(der.as_bytes());
143    Ok(URL_SAFE_NO_PAD.encode(digest))
144}