use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use base64::Engine as _;
use rsa::pkcs1::DecodeRsaPrivateKey as _;
use rsa::pkcs1v15::Pkcs1v15Sign;
use rsa::pkcs8::DecodePrivateKey as _;
use rsa::RsaPrivateKey;
use sha2::{Digest as _, Sha256};
use super::Error;
pub struct JwtConfig {
pub client_id: String,
pub client_secret: String,
pub public_key_id: String,
pub private_key_pem: Vec<u8>,
pub passphrase: Option<String>,
pub enterprise_id: String,
pub user_id: Option<String>,
pub token_url: Option<String>,
}
static JTI_COUNTER: AtomicU64 = AtomicU64::new(0);
pub(crate) struct Signer {
key: RsaPrivateKey,
client_id: String,
public_key_id: String,
subject_type: &'static str,
subject_id: String,
}
impl Signer {
pub(crate) fn new(config: &JwtConfig) -> Result<Signer, Error> {
let key = parse_rsa_private_key(&config.private_key_pem, config.passphrase.as_deref())?;
let (subject_type, subject_id) = match &config.user_id {
Some(user) => ("user", user.clone()),
None => ("enterprise", config.enterprise_id.clone()),
};
Ok(Signer {
key,
client_id: config.client_id.clone(),
public_key_id: config.public_key_id.clone(),
subject_type,
subject_id,
})
}
pub(crate) fn assertion(&self, audience: &str) -> Result<String, Error> {
let header = serde_json::json!({
"alg": "RS256",
"typ": "JWT",
"kid": self.public_key_id,
});
let claims = serde_json::json!({
"iss": self.client_id,
"sub": self.subject_id,
"box_sub_type": self.subject_type,
"aud": audience,
"jti": jti(),
"exp": now_unix() + 45,
});
let signing_input = format!(
"{}.{}",
b64(&serde_json::to_vec(&header)?),
b64(&serde_json::to_vec(&claims)?)
);
let digest = Sha256::digest(signing_input.as_bytes());
let signature = self
.key
.sign(Pkcs1v15Sign::new::<Sha256>(), &digest)
.map_err(|e| Error::new(format!("gantryruntime: signing assertion: {e}")))?;
Ok(format!("{signing_input}.{}", b64(&signature)))
}
}
fn parse_rsa_private_key(pem: &[u8], passphrase: Option<&str>) -> Result<RsaPrivateKey, Error> {
let text = std::str::from_utf8(pem)
.map_err(|_| Error::new("gantryruntime: private key PEM is not valid UTF-8"))?;
if text.contains("ENCRYPTED PRIVATE KEY") {
let passphrase = passphrase.ok_or_else(|| {
Error::new("gantryruntime: private key is encrypted but no passphrase was given")
})?;
return RsaPrivateKey::from_pkcs8_encrypted_pem(text, passphrase.as_bytes())
.map_err(|e| Error::new(format!("gantryruntime: decrypting private key: {e}")));
}
if let Ok(key) = RsaPrivateKey::from_pkcs8_pem(text) {
return Ok(key);
}
RsaPrivateKey::from_pkcs1_pem(text)
.map_err(|e| Error::new(format!("gantryruntime: parsing private key: {e}")))
}
fn b64(bytes: &[u8]) -> String {
URL_SAFE_NO_PAD.encode(bytes)
}
fn now_unix() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0)
}
fn jti() -> String {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let counter = JTI_COUNTER.fetch_add(1, Ordering::Relaxed);
format!("{nanos:032x}{counter:016x}")
}