box_open_sdk/runtime/
jwt.rs1use std::sync::atomic::{AtomicU64, Ordering};
11use std::time::{SystemTime, UNIX_EPOCH};
12
13use base64::engine::general_purpose::URL_SAFE_NO_PAD;
14use base64::Engine as _;
15use rsa::pkcs1::DecodeRsaPrivateKey as _;
16use rsa::pkcs1v15::Pkcs1v15Sign;
17use rsa::pkcs8::DecodePrivateKey as _;
18use rsa::RsaPrivateKey;
19use sha2::{Digest as _, Sha256};
20
21use super::Error;
22
23pub struct JwtConfig {
27 pub client_id: String,
28 pub client_secret: String,
29 pub public_key_id: String,
31 pub private_key_pem: Vec<u8>,
33 pub passphrase: Option<String>,
35 pub enterprise_id: String,
36 pub user_id: Option<String>,
38 pub token_url: Option<String>,
40}
41
42static JTI_COUNTER: AtomicU64 = AtomicU64::new(0);
45
46pub(crate) struct Signer {
49 key: RsaPrivateKey,
50 client_id: String,
51 public_key_id: String,
52 subject_type: &'static str,
53 subject_id: String,
54}
55
56impl Signer {
57 pub(crate) fn new(config: &JwtConfig) -> Result<Signer, Error> {
60 let key = parse_rsa_private_key(&config.private_key_pem, config.passphrase.as_deref())?;
61 let (subject_type, subject_id) = match &config.user_id {
62 Some(user) => ("user", user.clone()),
63 None => ("enterprise", config.enterprise_id.clone()),
64 };
65 Ok(Signer {
66 key,
67 client_id: config.client_id.clone(),
68 public_key_id: config.public_key_id.clone(),
69 subject_type,
70 subject_id,
71 })
72 }
73
74 pub(crate) fn assertion(&self, audience: &str) -> Result<String, Error> {
77 let header = serde_json::json!({
78 "alg": "RS256",
79 "typ": "JWT",
80 "kid": self.public_key_id,
81 });
82 let claims = serde_json::json!({
83 "iss": self.client_id,
84 "sub": self.subject_id,
85 "box_sub_type": self.subject_type,
86 "aud": audience,
87 "jti": jti(),
88 "exp": now_unix() + 45,
89 });
90 let signing_input = format!(
91 "{}.{}",
92 b64(&serde_json::to_vec(&header)?),
93 b64(&serde_json::to_vec(&claims)?)
94 );
95 let digest = Sha256::digest(signing_input.as_bytes());
96 let signature = self
97 .key
98 .sign(Pkcs1v15Sign::new::<Sha256>(), &digest)
99 .map_err(|e| Error::new(format!("gantryruntime: signing assertion: {e}")))?;
100 Ok(format!("{signing_input}.{}", b64(&signature)))
101 }
102}
103
104fn parse_rsa_private_key(pem: &[u8], passphrase: Option<&str>) -> Result<RsaPrivateKey, Error> {
107 let text = std::str::from_utf8(pem)
108 .map_err(|_| Error::new("gantryruntime: private key PEM is not valid UTF-8"))?;
109 if text.contains("ENCRYPTED PRIVATE KEY") {
110 let passphrase = passphrase.ok_or_else(|| {
111 Error::new("gantryruntime: private key is encrypted but no passphrase was given")
112 })?;
113 return RsaPrivateKey::from_pkcs8_encrypted_pem(text, passphrase.as_bytes())
114 .map_err(|e| Error::new(format!("gantryruntime: decrypting private key: {e}")));
115 }
116 if let Ok(key) = RsaPrivateKey::from_pkcs8_pem(text) {
117 return Ok(key);
118 }
119 RsaPrivateKey::from_pkcs1_pem(text)
120 .map_err(|e| Error::new(format!("gantryruntime: parsing private key: {e}")))
121}
122
123fn b64(bytes: &[u8]) -> String {
125 URL_SAFE_NO_PAD.encode(bytes)
126}
127
128fn now_unix() -> u64 {
130 SystemTime::now()
131 .duration_since(UNIX_EPOCH)
132 .map(|d| d.as_secs())
133 .unwrap_or(0)
134}
135
136fn jti() -> String {
139 let nanos = SystemTime::now()
140 .duration_since(UNIX_EPOCH)
141 .map(|d| d.as_nanos())
142 .unwrap_or(0);
143 let counter = JTI_COUNTER.fetch_add(1, Ordering::Relaxed);
144 format!("{nanos:032x}{counter:016x}")
145}