use std::time::{SystemTime, UNIX_EPOCH};
use aws_lc_rs::encoding::AsDer;
use aws_lc_rs::rand::SystemRandom;
use aws_lc_rs::rsa::KeyPair;
use aws_lc_rs::signature::{KeyPair as _, RSA_PKCS1_SHA256};
use base64::engine::general_purpose::{STANDARD, URL_SAFE_NO_PAD};
use base64::Engine as _;
use serde_json::json;
use sha2::{Digest, Sha256};
use super::error::{Error, Result};
const JWT_LIFETIME_SECS: u64 = 3540;
pub(crate) fn build_jwt(account: &str, user: &str, private_key_pem: &str) -> Result<String> {
let key_pair = load_key_pair(private_key_pem)?;
let fingerprint = public_key_fingerprint(&key_pair)?;
let qualified = format!(
"{}.{}",
normalize_account(account),
user.trim().to_uppercase()
);
let iss = format!("{qualified}.{fingerprint}");
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_err(|_| Error::Auth("system clock is before the Unix epoch".into()))?
.as_secs();
let header = encode_part(&json!({ "alg": "RS256", "typ": "JWT" }))?;
let claims = encode_part(&json!({
"iss": iss,
"sub": qualified,
"iat": now,
"exp": now + JWT_LIFETIME_SECS,
}))?;
let signing_input = format!("{header}.{claims}");
let signature = sign(&key_pair, signing_input.as_bytes())?;
Ok(format!(
"{signing_input}.{}",
URL_SAFE_NO_PAD.encode(signature)
))
}
fn normalize_account(account: &str) -> String {
let account = account.trim();
let base = if account.contains(".global") {
account.split('-').next()
} else {
account.split('.').next()
};
base.unwrap_or_default().to_uppercase()
}
fn load_key_pair(pem: &str) -> Result<KeyPair> {
let der = pem_to_der(pem)?;
KeyPair::from_pkcs8(&der).map_err(|e| {
Error::Auth(format!(
"invalid RSA private key (expected unencrypted PKCS#8): {e}"
))
})
}
fn pem_to_der(pem: &str) -> Result<Vec<u8>> {
if pem.contains("ENCRYPTED PRIVATE KEY") {
return Err(Error::Auth(
"encrypted private keys are not yet supported; decrypt with \
`openssl pkcs8 -in key.p8 -out key_unencrypted.p8`"
.into(),
));
}
let body: String = pem
.lines()
.filter(|line| !line.trim_start().starts_with("-----"))
.collect();
if body.trim().is_empty() {
return Err(Error::Auth(
"private key PEM is empty or missing its base64 body".into(),
));
}
STANDARD
.decode(body.trim())
.map_err(|e| Error::Auth(format!("private key PEM body is not valid base64: {e}")))
}
fn public_key_fingerprint(key_pair: &KeyPair) -> Result<String> {
let spki = key_pair
.public_key()
.as_der()
.map_err(|_| Error::Auth("failed to export the RSA public key".into()))?;
Ok(format!(
"SHA256:{}",
STANDARD.encode(Sha256::digest(spki.as_ref()))
))
}
fn sign(key_pair: &KeyPair, msg: &[u8]) -> Result<Vec<u8>> {
let mut signature = vec![0u8; key_pair.public_modulus_len()];
key_pair
.sign(&RSA_PKCS1_SHA256, &SystemRandom::new(), msg, &mut signature)
.map_err(|_| Error::Auth("failed to sign the key-pair JWT".into()))?;
Ok(signature)
}
fn encode_part(value: &serde_json::Value) -> Result<String> {
let bytes =
serde_json::to_vec(value).map_err(|e| Error::Auth(format!("serializing JWT: {e}")))?;
Ok(URL_SAFE_NO_PAD.encode(bytes))
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
use aws_lc_rs::rsa::KeySize;
use aws_lc_rs::signature::{UnparsedPublicKey, RSA_PKCS1_2048_8192_SHA256};
fn generate_key_pem() -> (String, KeyPair) {
let key_pair = KeyPair::generate(KeySize::Rsa2048).unwrap();
let der = key_pair.as_der().unwrap();
let pem = format!(
"-----BEGIN PRIVATE KEY-----\n{}\n-----END PRIVATE KEY-----\n",
STANDARD.encode(der.as_ref())
);
(pem, key_pair)
}
#[test]
fn build_jwt_produces_verifiable_claims_and_signature() {
let (pem, key_pair) = generate_key_pem();
let jwt = build_jwt("myacct.us-east-1.aws", "svc_user", &pem).unwrap();
let parts: Vec<&str> = jwt.split('.').collect();
assert_eq!(parts.len(), 3, "compact JWS has three segments");
let claims: serde_json::Value =
serde_json::from_slice(&URL_SAFE_NO_PAD.decode(parts[1]).unwrap()).unwrap();
assert_eq!(claims["sub"], "MYACCT.SVC_USER");
let iss = claims["iss"].as_str().unwrap();
assert!(iss.starts_with("MYACCT.SVC_USER.SHA256:"));
assert!(claims["exp"].as_u64().unwrap() > claims["iat"].as_u64().unwrap());
let spki = key_pair.public_key().as_der().unwrap();
let expected_fp = format!("SHA256:{}", STANDARD.encode(Sha256::digest(spki.as_ref())));
assert!(iss.ends_with(&expected_fp));
let signing_input = format!("{}.{}", parts[0], parts[1]);
let signature = URL_SAFE_NO_PAD.decode(parts[2]).unwrap();
UnparsedPublicKey::new(&RSA_PKCS1_2048_8192_SHA256, spki.as_ref())
.verify(signing_input.as_bytes(), &signature)
.expect("signature must verify against the registered public key");
}
#[test]
fn build_jwt_rejects_encrypted_keys() {
let pem =
"-----BEGIN ENCRYPTED PRIVATE KEY-----\nAAAA\n-----END ENCRYPTED PRIVATE KEY-----";
let err = build_jwt("acct", "user", pem).unwrap_err();
assert!(err.to_string().contains("encrypted"));
}
#[test]
fn build_jwt_rejects_garbage_keys() {
assert!(build_jwt("acct", "user", "not a pem at all").is_err());
assert!(build_jwt("acct", "user", "").is_err());
}
#[test]
fn normalize_account_strips_region_and_uppercases() {
assert_eq!(normalize_account("xy12345.us-east-1.aws"), "XY12345");
assert_eq!(normalize_account("myorg-acct"), "MYORG-ACCT");
assert_eq!(normalize_account(" Lower "), "LOWER");
}
#[test]
fn normalize_account_partitions_global_urls_on_the_dash() {
assert_eq!(normalize_account("myaccount-myorg.global"), "MYACCOUNT");
assert_eq!(
normalize_account("acct123-org456.global.snowflakecomputing.com"),
"ACCT123"
);
}
}