1#[cfg(feature = "native-hydra")]
23use aws_lc_rs::{
24 rand::SystemRandom,
25 signature::{RSA_PKCS1_SHA256, RsaKeyPair},
26};
27#[cfg(feature = "native-hydra")]
28use rustls_pki_types::{PrivateKeyDer, pem::PemObject};
29#[cfg(feature = "native-hydra")]
30use serde_json::json;
31
32use crate::error::{Error, ErrorKind, Result};
33#[cfg(feature = "native-hydra")]
34use crate::jwt::b64url;
35use crate::jwt::now;
36
37#[cfg(feature = "native-hydra")]
38const ISS: &str = "gaa";
39#[cfg(feature = "native-hydra")]
40const AUD: &str = "gfw";
41#[cfg(feature = "native-hydra")]
42const NAME: &str = "grabtaxi";
43pub const DEFAULT_VER: &str = "1.19.0.24";
45#[cfg(feature = "native-hydra")]
47const TTL_SECS: i64 = 600;
48
49#[derive(Debug, Clone)]
52pub struct DeviceFields {
53 pub br_id: String,
54 pub br_id_v2: String,
55 pub br_uid: String,
56 pub suspicious: bool,
57}
58
59impl DeviceFields {
60 pub fn random() -> Self {
62 Self {
63 br_id: format!("{}y6r", hex(17)),
64 br_id_v2: format!("{}y6r", hex(17)),
65 br_uid: uuid_v4(),
66 suspicious: false,
67 }
68 }
69}
70
71fn rand_bytes(n: usize) -> Vec<u8> {
72 let mut buf = vec![0u8; n];
73 if getrandom::fill(&mut buf).is_err() {
76 let seed = now() as u64 ^ (std::process::id() as u64);
77 for (i, b) in buf.iter_mut().enumerate() {
78 *b = (seed
79 .wrapping_mul(6364136223846793005)
80 .wrapping_add(i as u64)
81 >> 33) as u8;
82 }
83 }
84 buf
85}
86
87fn hex(n: usize) -> String {
88 rand_bytes(n.div_ceil(2))
89 .iter()
90 .map(|b| format!("{b:02x}"))
91 .collect::<String>()
92 .chars()
93 .take(n)
94 .collect()
95}
96
97fn uuid_v4() -> String {
98 let mut b = rand_bytes(16);
99 b[6] = (b[6] & 0x0f) | 0x40;
100 b[8] = (b[8] & 0x3f) | 0x80;
101 let h: Vec<String> = b.iter().map(|x| format!("{x:02x}")).collect();
102 format!(
103 "{}{}{}{}-{}{}-{}{}-{}{}-{}{}{}{}{}{}",
104 h[0],
105 h[1],
106 h[2],
107 h[3],
108 h[4],
109 h[5],
110 h[6],
111 h[7],
112 h[8],
113 h[9],
114 h[10],
115 h[11],
116 h[12],
117 h[13],
118 h[14],
119 h[15]
120 )
121}
122
123#[cfg(feature = "native-hydra")]
124fn load_key(pem: &str) -> Result<RsaKeyPair> {
125 let invalid = |detail: String| {
126 Error::new(
127 ErrorKind::HydraExpired,
128 format!("could not read hydra signing key: {detail}"),
129 )
130 };
131 let der = PrivateKeyDer::from_pem_slice(pem.as_bytes()).map_err(|e| invalid(e.to_string()))?;
132 match der {
133 PrivateKeyDer::Pkcs1(key) => RsaKeyPair::from_der(key.secret_pkcs1_der()),
134 PrivateKeyDer::Pkcs8(key) => RsaKeyPair::from_pkcs8(key.secret_pkcs8_der()),
135 _ => {
136 return Err(invalid(
137 "expected an RSA PKCS#1 or PKCS#8 private key".into(),
138 ));
139 }
140 }
141 .map_err(|e| invalid(e.to_string()))
142}
143
144#[cfg(feature = "native-hydra")]
147pub fn mint_native(key_pem: &str, fields: &DeviceFields, ver: &str) -> Result<(String, i64)> {
148 let key = load_key(key_pem)?;
149
150 let iat = now();
151 let exp = iat + TTL_SECS;
152 let header = json!({"alg": "RS256", "typ": "JWT"});
153 let claims = json!({
154 "iss": ISS,
155 "aud": AUD,
156 "name": NAME,
157 "iat": iat,
158 "exp": exp,
159 "nbf": iat,
160 "ver": ver,
161 "brID": fields.br_id,
162 "sus": fields.suspicious,
163 "brIDv2": fields.br_id_v2,
164 "brUID": fields.br_uid,
165 });
166
167 let signing_input = format!(
168 "{}.{}",
169 b64url(&serde_json::to_vec(&header)?),
170 b64url(&serde_json::to_vec(&claims)?)
171 );
172 let mut signature = vec![0; key.public_modulus_len()];
173 key.sign(
174 &RSA_PKCS1_SHA256,
175 &SystemRandom::new(),
176 signing_input.as_bytes(),
177 &mut signature,
178 )
179 .map_err(|_| Error::new(ErrorKind::HydraExpired, "could not sign hydra JWT"))?;
180 let token = format!("{signing_input}.{}", b64url(&signature));
181 Ok((token, exp))
182}
183
184#[cfg(not(feature = "native-hydra"))]
187pub fn mint_native(_key_pem: &str, _fields: &DeviceFields, _ver: &str) -> Result<(String, i64)> {
188 Err(Error::new(
189 ErrorKind::HydraExpired,
190 "native hydra minting is not built in (grabcli-core without the \
191 `native-hydra` feature); set hydra_mode = \"cdp\"",
192 ))
193}
194
195#[cfg(all(test, feature = "native-hydra"))]
196pub(crate) mod tests {
197 use super::*;
198 use aws_lc_rs::{
199 encoding::{AsDer, Pkcs8V1Der},
200 rsa::KeySize,
201 signature::{KeyPair, RSA_PKCS1_2048_8192_SHA256, UnparsedPublicKey},
202 };
203
204 pub(crate) fn test_key() -> (String, RsaKeyPair) {
206 use base64::Engine as _;
207 let key = RsaKeyPair::generate(KeySize::Rsa2048).expect("keygen");
208 let der: Pkcs8V1Der<'static> = key.as_der().expect("DER");
209 let body = base64::engine::general_purpose::STANDARD.encode(der.as_ref());
210 let pem = format!("-----BEGIN PRIVATE KEY-----\n{body}\n-----END PRIVATE KEY-----\n");
211 (pem, key)
212 }
213
214 #[test]
215 fn accepts_pkcs1_and_pkcs8_pem_keys() {
216 use base64::Engine as _;
217 let (pkcs8_pem, key) = test_key();
218 let der: Pkcs8V1Der<'static> = key.as_der().expect("DER");
219 let info = pkcs8::PrivateKeyInfoRef::try_from(der.as_ref()).expect("PKCS#8");
220 let body = base64::engine::general_purpose::STANDARD.encode(info.private_key.as_bytes());
221 let pkcs1_pem =
222 format!("-----BEGIN RSA PRIVATE KEY-----\n{body}\n-----END RSA PRIVATE KEY-----\n");
223 assert!(load_key(&pkcs8_pem).is_ok());
224 assert!(load_key(&pkcs1_pem).is_ok());
225 }
226
227 #[test]
228 fn mints_a_verifiable_rs256_jwt() -> Result<()> {
229 let (pem, key) = test_key();
230 let fields = DeviceFields::random();
231 let (token, exp) = mint_native(&pem, &fields, DEFAULT_VER)?;
232
233 let parts: Vec<&str> = token.split('.').collect();
234 assert_eq!(parts.len(), 3);
235
236 let claims = crate::jwt::payload(&token)?;
238 assert_eq!(claims["iss"], "gaa");
239 assert_eq!(claims["aud"], "gfw");
240 assert_eq!(claims["name"], "grabtaxi");
241 assert_eq!(claims["ver"], DEFAULT_VER);
242 assert_eq!(claims["sus"], false);
243 assert_eq!(exp - claims["iat"].as_i64().unwrap_or(0), 600);
244
245 let vk = UnparsedPublicKey::new(&RSA_PKCS1_2048_8192_SHA256, key.public_key().as_ref());
247 let signing_input = format!("{}.{}", parts[0], parts[1]);
248 let sig_bytes =
249 base64::Engine::decode(&base64::engine::general_purpose::URL_SAFE_NO_PAD, parts[2])
250 .expect("sig b64");
251 assert!(vk.verify(signing_input.as_bytes(), &sig_bytes).is_ok());
252 Ok(())
253 }
254
255 #[test]
256 fn device_fields_look_right() {
257 let f = DeviceFields::random();
258 assert_eq!(f.br_id.len(), 20);
259 assert!(f.br_uid.contains('-'));
260 assert_eq!(f.br_uid.len(), 36);
261 }
262
263 #[test]
264 fn bad_key_is_hydra_error() {
265 let e = mint_native("not a pem", &DeviceFields::random(), DEFAULT_VER)
266 .expect_err("should error");
267 assert_eq!(e.kind, ErrorKind::HydraExpired);
268 }
269}