use aws_lc_rs::rand::SystemRandom;
use aws_lc_rs::signature::{RsaKeyPair, RSA_PKCS1_SHA256};
use serde_json::Value;
use crate::auth::AuthError;
pub fn b64url(data: &[u8]) -> String {
const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
let mut out = String::with_capacity(data.len().div_ceil(3) * 4);
for chunk in data.chunks(3) {
let mut buf = [0u8; 3];
buf[..chunk.len()].copy_from_slice(chunk);
let n = u32::from_be_bytes([0, buf[0], buf[1], buf[2]]);
for i in 0..=chunk.len() {
out.push(ALPHABET[((n >> (18 - 6 * i)) & 63) as usize] as char);
}
}
out
}
fn not_configured(message: String, hint: Option<&str>) -> AuthError {
AuthError::NotConfigured {
provider: None,
message,
hint: hint.map(str::to_string),
}
}
pub fn pem_block(text: &str, label: &str) -> Result<Option<Vec<u8>>, AuthError> {
let head = format!("-----BEGIN {label}-----");
let tail = format!("-----END {label}-----");
let Some(start) = text.find(&head) else {
return Ok(None);
};
let after = start + head.len();
let Some(end) = text[after..].find(&tail) else {
return Err(not_configured(
format!("PEM block {label:?} has no END line"),
None,
));
};
let body: String = text[after..after + end].split_whitespace().collect();
crate::types::base64_decode(&body)
.map(Some)
.map_err(|err| not_configured(format!("PEM block {label:?}: {}", err.message), None))
}
pub fn load_private_key(pem: &str) -> Result<RsaKeyPair, AuthError> {
if pem.contains("ENCRYPTED") {
return Err(not_configured(
"encrypted private keys are not supported; decrypt with `openssl pkey`".into(),
Some("openssl pkey -in key.pem -out key-plain.pem"),
));
}
if let Some(der) = pem_block(pem, "PRIVATE KEY")? {
return RsaKeyPair::from_pkcs8(&der)
.map_err(|err| not_configured(format!("PKCS#8 private key rejected: {err}"), None));
}
if let Some(der) = pem_block(pem, "RSA PRIVATE KEY")? {
return RsaKeyPair::from_der(&der)
.map_err(|err| not_configured(format!("PKCS#1 private key rejected: {err}"), None));
}
Err(not_configured(
"no PRIVATE KEY / RSA PRIVATE KEY block in the PEM (PKCS#12? use `openssl pkcs12 -nodes`)"
.into(),
None,
))
}
pub fn certificate_der(pem: &str) -> Result<Vec<u8>, AuthError> {
pem_block(pem, "CERTIFICATE")?
.ok_or_else(|| not_configured("no CERTIFICATE block in the PEM".into(), None))
}
pub fn x5t(der: &[u8]) -> String {
let digest = aws_lc_rs::digest::digest(&aws_lc_rs::digest::SHA1_FOR_LEGACY_USE_ONLY, der);
b64url(digest.as_ref())
}
pub fn sign(key: &RsaKeyPair, message: &[u8]) -> Result<Vec<u8>, AuthError> {
let mut signature = vec![0u8; key.public_modulus_len()];
key.sign(
&RSA_PKCS1_SHA256,
&SystemRandom::new(),
message,
&mut signature,
)
.map_err(|_| not_configured("RS256 signing failed".into(), None))?;
Ok(signature)
}
pub fn jwt_encode(header: &Value, payload: &Value, key: &RsaKeyPair) -> Result<String, AuthError> {
let signing_input = format!(
"{}.{}",
b64url(
serde_json::to_string(header)
.expect("serializes")
.as_bytes()
),
b64url(
serde_json::to_string(payload)
.expect("serializes")
.as_bytes()
)
);
let signature = sign(key, signing_input.as_bytes())?;
Ok(format!("{signing_input}.{}", b64url(&signature)))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn b64url_is_unpadded() {
assert_eq!(b64url(b""), "");
assert_eq!(b64url(b"f"), "Zg");
assert_eq!(b64url(b"fo"), "Zm8");
assert_eq!(b64url(b"foo"), "Zm9v");
assert_eq!(b64url(&[0xfb, 0xff]), "-_8");
}
}