#[cfg(feature = "native-hydra")]
use aws_lc_rs::{
rand::SystemRandom,
signature::{RSA_PKCS1_SHA256, RsaKeyPair},
};
#[cfg(feature = "native-hydra")]
use rustls_pki_types::{PrivateKeyDer, pem::PemObject};
#[cfg(feature = "native-hydra")]
use serde_json::json;
use crate::error::{Error, ErrorKind, Result};
#[cfg(feature = "native-hydra")]
use crate::jwt::b64url;
use crate::jwt::now;
#[cfg(feature = "native-hydra")]
const ISS: &str = "gaa";
#[cfg(feature = "native-hydra")]
const AUD: &str = "gfw";
#[cfg(feature = "native-hydra")]
const NAME: &str = "grabtaxi";
pub const DEFAULT_VER: &str = "1.19.0.24";
#[cfg(feature = "native-hydra")]
const TTL_SECS: i64 = 600;
#[derive(Debug, Clone)]
pub struct DeviceFields {
pub br_id: String,
pub br_id_v2: String,
pub br_uid: String,
pub suspicious: bool,
}
impl DeviceFields {
pub fn random() -> Self {
Self {
br_id: format!("{}y6r", hex(17)),
br_id_v2: format!("{}y6r", hex(17)),
br_uid: uuid_v4(),
suspicious: false,
}
}
}
fn rand_bytes(n: usize) -> Vec<u8> {
let mut buf = vec![0u8; n];
if getrandom::fill(&mut buf).is_err() {
let seed = now() as u64 ^ (std::process::id() as u64);
for (i, b) in buf.iter_mut().enumerate() {
*b = (seed
.wrapping_mul(6364136223846793005)
.wrapping_add(i as u64)
>> 33) as u8;
}
}
buf
}
fn hex(n: usize) -> String {
rand_bytes(n.div_ceil(2))
.iter()
.map(|b| format!("{b:02x}"))
.collect::<String>()
.chars()
.take(n)
.collect()
}
fn uuid_v4() -> String {
let mut b = rand_bytes(16);
b[6] = (b[6] & 0x0f) | 0x40;
b[8] = (b[8] & 0x3f) | 0x80;
let h: Vec<String> = b.iter().map(|x| format!("{x:02x}")).collect();
format!(
"{}{}{}{}-{}{}-{}{}-{}{}-{}{}{}{}{}{}",
h[0],
h[1],
h[2],
h[3],
h[4],
h[5],
h[6],
h[7],
h[8],
h[9],
h[10],
h[11],
h[12],
h[13],
h[14],
h[15]
)
}
#[cfg(feature = "native-hydra")]
fn load_key(pem: &str) -> Result<RsaKeyPair> {
let invalid = |detail: String| {
Error::new(
ErrorKind::HydraExpired,
format!("could not read hydra signing key: {detail}"),
)
};
let der = PrivateKeyDer::from_pem_slice(pem.as_bytes()).map_err(|e| invalid(e.to_string()))?;
match der {
PrivateKeyDer::Pkcs1(key) => RsaKeyPair::from_der(key.secret_pkcs1_der()),
PrivateKeyDer::Pkcs8(key) => RsaKeyPair::from_pkcs8(key.secret_pkcs8_der()),
_ => {
return Err(invalid(
"expected an RSA PKCS#1 or PKCS#8 private key".into(),
));
}
}
.map_err(|e| invalid(e.to_string()))
}
#[cfg(feature = "native-hydra")]
pub fn mint_native(key_pem: &str, fields: &DeviceFields, ver: &str) -> Result<(String, i64)> {
let key = load_key(key_pem)?;
let iat = now();
let exp = iat + TTL_SECS;
let header = json!({"alg": "RS256", "typ": "JWT"});
let claims = json!({
"iss": ISS,
"aud": AUD,
"name": NAME,
"iat": iat,
"exp": exp,
"nbf": iat,
"ver": ver,
"brID": fields.br_id,
"sus": fields.suspicious,
"brIDv2": fields.br_id_v2,
"brUID": fields.br_uid,
});
let signing_input = format!(
"{}.{}",
b64url(&serde_json::to_vec(&header)?),
b64url(&serde_json::to_vec(&claims)?)
);
let mut signature = vec![0; key.public_modulus_len()];
key.sign(
&RSA_PKCS1_SHA256,
&SystemRandom::new(),
signing_input.as_bytes(),
&mut signature,
)
.map_err(|_| Error::new(ErrorKind::HydraExpired, "could not sign hydra JWT"))?;
let token = format!("{signing_input}.{}", b64url(&signature));
Ok((token, exp))
}
#[cfg(not(feature = "native-hydra"))]
pub fn mint_native(_key_pem: &str, _fields: &DeviceFields, _ver: &str) -> Result<(String, i64)> {
Err(Error::new(
ErrorKind::HydraExpired,
"native hydra minting is not built in (grabcli-core without the \
`native-hydra` feature); set hydra_mode = \"cdp\"",
))
}
#[cfg(all(test, feature = "native-hydra"))]
pub(crate) mod tests {
use super::*;
use aws_lc_rs::{
encoding::{AsDer, Pkcs8V1Der},
rsa::KeySize,
signature::{KeyPair, RSA_PKCS1_2048_8192_SHA256, UnparsedPublicKey},
};
pub(crate) fn test_key() -> (String, RsaKeyPair) {
use base64::Engine as _;
let key = RsaKeyPair::generate(KeySize::Rsa2048).expect("keygen");
let der: Pkcs8V1Der<'static> = key.as_der().expect("DER");
let body = base64::engine::general_purpose::STANDARD.encode(der.as_ref());
let pem = format!("-----BEGIN PRIVATE KEY-----\n{body}\n-----END PRIVATE KEY-----\n");
(pem, key)
}
#[test]
fn accepts_pkcs1_and_pkcs8_pem_keys() {
use base64::Engine as _;
let (pkcs8_pem, key) = test_key();
let der: Pkcs8V1Der<'static> = key.as_der().expect("DER");
let info = pkcs8::PrivateKeyInfoRef::try_from(der.as_ref()).expect("PKCS#8");
let body = base64::engine::general_purpose::STANDARD.encode(info.private_key.as_bytes());
let pkcs1_pem =
format!("-----BEGIN RSA PRIVATE KEY-----\n{body}\n-----END RSA PRIVATE KEY-----\n");
assert!(load_key(&pkcs8_pem).is_ok());
assert!(load_key(&pkcs1_pem).is_ok());
}
#[test]
fn mints_a_verifiable_rs256_jwt() -> Result<()> {
let (pem, key) = test_key();
let fields = DeviceFields::random();
let (token, exp) = mint_native(&pem, &fields, DEFAULT_VER)?;
let parts: Vec<&str> = token.split('.').collect();
assert_eq!(parts.len(), 3);
let claims = crate::jwt::payload(&token)?;
assert_eq!(claims["iss"], "gaa");
assert_eq!(claims["aud"], "gfw");
assert_eq!(claims["name"], "grabtaxi");
assert_eq!(claims["ver"], DEFAULT_VER);
assert_eq!(claims["sus"], false);
assert_eq!(exp - claims["iat"].as_i64().unwrap_or(0), 600);
let vk = UnparsedPublicKey::new(&RSA_PKCS1_2048_8192_SHA256, key.public_key().as_ref());
let signing_input = format!("{}.{}", parts[0], parts[1]);
let sig_bytes =
base64::Engine::decode(&base64::engine::general_purpose::URL_SAFE_NO_PAD, parts[2])
.expect("sig b64");
assert!(vk.verify(signing_input.as_bytes(), &sig_bytes).is_ok());
Ok(())
}
#[test]
fn device_fields_look_right() {
let f = DeviceFields::random();
assert_eq!(f.br_id.len(), 20);
assert!(f.br_uid.contains('-'));
assert_eq!(f.br_uid.len(), 36);
}
#[test]
fn bad_key_is_hydra_error() {
let e = mint_native("not a pem", &DeviceFields::random(), DEFAULT_VER)
.expect_err("should error");
assert_eq!(e.kind, ErrorKind::HydraExpired);
}
}