1use std::collections::HashMap;
9
10use base64::engine::general_purpose::URL_SAFE_NO_PAD;
11use base64::Engine;
12use jsonwebtoken::{decode, decode_header, DecodingKey, Validation};
13use serde::{Deserialize, Serialize};
14use serde_json::Value;
15use sha2::{Digest, Sha256};
16
17use crate::bridge_oauth::{project_jwk_to_public_key, Jwk, Jwks};
18use crate::bridges::{Bridge, BridgeError, BridgeKind};
19use crate::generated::{
20 ActorIdentity, ActorIdentity_IdentityVersion, ActorType, AuthorityRoot, AuthorityRoot_Kind,
21 TrustLevel,
22};
23
24#[derive(Clone, Debug, Serialize, Deserialize)]
25pub struct GnapKeyDescriptor {
26 pub proof: String,
27 pub jwk: Jwk,
28}
29
30#[derive(Clone, Debug, Serialize, Deserialize)]
31pub struct GnapClient {
32 #[serde(skip_serializing_if = "Option::is_none", default)]
33 pub id: Option<String>,
34 pub key: GnapKeyDescriptor,
35}
36
37#[derive(Clone, Debug, Serialize, Deserialize)]
38#[serde(untagged)]
39pub enum GnapAccessRight {
40 Reference(String),
41 Object {
42 #[serde(skip_serializing_if = "Option::is_none", default)]
43 actions: Option<Vec<String>>,
44 #[serde(skip_serializing_if = "Option::is_none", default)]
45 locations: Option<Vec<String>>,
46 #[serde(skip_serializing_if = "Option::is_none", default, rename = "type")]
47 kind: Option<String>,
48 },
49}
50
51impl GnapAccessRight {
52 pub fn actions(&self) -> Vec<String> {
53 match self {
54 GnapAccessRight::Reference(s) => vec![s.clone()],
55 GnapAccessRight::Object { actions, .. } => actions.clone().unwrap_or_default(),
56 }
57 }
58}
59
60#[derive(Clone, Debug, Serialize, Deserialize)]
61pub struct GnapAccessTokenRequest {
62 pub access: Vec<GnapAccessRight>,
63}
64
65#[derive(Clone, Debug, Serialize, Deserialize)]
66pub struct GnapGrantRequest {
67 pub client: GnapClient,
68 pub access_token: GnapAccessTokenRequest,
69}
70
71#[derive(Clone, Debug, Serialize, Deserialize)]
72pub struct GnapAccessTokenResponse {
73 pub value: String,
74 pub bound: bool,
75 #[serde(skip_serializing_if = "Option::is_none", default)]
76 pub expires_in: Option<u64>,
77}
78
79#[derive(Clone, Debug, Serialize, Deserialize)]
80pub struct GnapGrantResponse {
81 pub access_token: GnapAccessTokenResponse,
82 #[serde(skip_serializing_if = "Option::is_none", default)]
83 pub continue_uri: Option<String>,
84}
85
86#[derive(Clone, Debug, Serialize, Deserialize)]
87pub struct GnapBridgeConfig {
88 pub bridge_id: String,
89 pub trust_domain: String,
90 pub issuer: String,
91 pub allowed_algorithms: Vec<String>,
92 pub jwks: Jwks,
93}
94
95#[derive(Clone, Debug)]
96pub struct GnapVerifiedGrant {
97 pub identity: ActorIdentity,
98 pub capabilities: Vec<String>,
99 pub client_key_thumbprint: String,
100}
101
102#[derive(Clone, Debug, Serialize, Deserialize)]
103pub struct DpopProofVerification {
104 pub ok: bool,
105 #[serde(skip_serializing_if = "Option::is_none", default)]
106 pub reason: Option<String>,
107 #[serde(skip_serializing_if = "Option::is_none", default)]
108 pub jkt_expected: Option<String>,
109 #[serde(skip_serializing_if = "Option::is_none", default)]
110 pub jkt_seen: Option<String>,
111}
112
113pub struct GnapBridge {
114 cfg: GnapBridgeConfig,
115}
116
117impl GnapBridge {
118 pub fn new(cfg: GnapBridgeConfig) -> Self {
119 GnapBridge { cfg }
120 }
121
122 pub fn build_grant_response(
123 &self,
124 req: &GnapGrantRequest,
125 token: &str,
126 finish_uri: Option<&str>,
127 ) -> Result<GnapGrantResponse, BridgeError> {
128 if req.access_token.access.is_empty() {
129 return Err(BridgeError::InvalidInput(
130 "access_token.access required".into(),
131 ));
132 }
133 if req.client.key.jwk.kty.is_empty() {
134 return Err(BridgeError::InvalidInput("client.key.jwk required".into()));
135 }
136 Ok(GnapGrantResponse {
137 access_token: GnapAccessTokenResponse {
138 value: token.into(),
139 bound: true,
140 expires_in: Some(600),
141 },
142 continue_uri: finish_uri.map(str::to_string),
143 })
144 }
145
146 pub fn verify_access_token(
147 &self,
148 token: &str,
149 request: &GnapGrantRequest,
150 ) -> Result<GnapVerifiedGrant, BridgeError> {
151 if token.is_empty() {
152 return Err(BridgeError::InvalidInput("missing access token".into()));
153 }
154 let header = decode_header(token)
155 .map_err(|e| BridgeError::Rejected(format!("malformed JWT: {}", e)))?;
156 let alg_name = format!("{:?}", header.alg);
157 if !self
158 .cfg
159 .allowed_algorithms
160 .iter()
161 .any(|a| a.eq_ignore_ascii_case(&alg_name))
162 {
163 return Err(BridgeError::Rejected(format!(
164 "algorithm {} not in allow-list",
165 alg_name
166 )));
167 }
168 let kid = header
169 .kid
170 .clone()
171 .ok_or_else(|| BridgeError::Rejected("JWT header missing kid".into()))?;
172 let jwk = self
173 .cfg
174 .jwks
175 .keys
176 .iter()
177 .find(|k| k.kid.as_deref() == Some(&kid))
178 .ok_or_else(|| BridgeError::Rejected(format!("no JWK with kid {}", kid)))?;
179 let key = decoding_key_for_jwk(jwk)?;
180 let mut validation = Validation::new(header.alg);
181 validation.set_issuer(&[self.cfg.issuer.as_str()]);
182 validation.algorithms = vec![header.alg];
183 validation.validate_aud = false;
184 let data = decode::<HashMap<String, Value>>(token, &key, &validation).map_err(|e| {
185 BridgeError::Rejected(format!("GNAP access token verify failed: {}", e))
186 })?;
187 let claims = data.claims;
188 let expected_jkt = jwk_thumbprint(&request.client.key.jwk)?;
189 if let Some(cnf) = claims.get("cnf").and_then(|v| v.as_object()) {
190 if let Some(jkt) = cnf.get("jkt").and_then(|v| v.as_str()) {
191 if jkt != expected_jkt {
192 return Err(BridgeError::Rejected(
193 "access token cnf.jkt does not match client.key".into(),
194 ));
195 }
196 }
197 }
198 let subject = claims
199 .get("sub")
200 .and_then(|v| v.as_str())
201 .unwrap_or("anonymous")
202 .to_string();
203 let actor_type_str = claims
204 .get("tf_actor_type")
205 .and_then(|v| v.as_str())
206 .unwrap_or("agent")
207 .to_string();
208 let actor_type = match actor_type_str.as_str() {
209 "human" => ActorType::Human,
210 "agent" => ActorType::Agent,
211 "device" => ActorType::Device,
212 "service" => ActorType::Service,
213 "site" => ActorType::Site,
214 "organization" => ActorType::Organization,
215 other => {
216 return Err(BridgeError::Rejected(format!(
217 "unsupported tf_actor_type: {}",
218 other
219 )))
220 }
221 };
222 let actor_id = format!(
223 "tf:actor:{}:{}/{}",
224 actor_type_str,
225 self.cfg.trust_domain,
226 url_encode(&subject)
227 );
228 let actions: Vec<String> = request
229 .access_token
230 .access
231 .iter()
232 .flat_map(|r| r.actions())
233 .collect();
234 let identity = ActorIdentity {
235 identity_version: ActorIdentity_IdentityVersion::V1,
236 actor_id,
237 actor_type,
238 instance_id: None,
239 public_keys: vec![project_jwk_to_public_key(&request.client.key.jwk)?],
240 trust_levels: vec![TrustLevel::T3],
241 authority_roots: vec![AuthorityRoot {
242 kind: AuthorityRoot_Kind::Organization,
243 id: self.cfg.issuer.clone(),
244 }],
245 attestations: None,
246 valid_from: claims
247 .get("iat")
248 .and_then(|v| v.as_u64())
249 .map(timestamp)
250 .unwrap_or_else(|| timestamp(now_unix())),
251 valid_until: claims.get("exp").and_then(|v| v.as_u64()).map(timestamp),
252 revocation_ref: None,
253 signature: None,
254 };
255 Ok(GnapVerifiedGrant {
256 identity,
257 capabilities: actions,
258 client_key_thumbprint: expected_jkt,
259 })
260 }
261
262 pub fn verify_dpop_proof(
263 &self,
264 proof_jwt: &str,
265 htm: &str,
266 htu: &str,
267 access_token_hash: Option<&str>,
268 expected_jkt: &str,
269 ) -> DpopProofVerification {
270 if proof_jwt.is_empty() {
271 return DpopProofVerification {
272 ok: false,
273 reason: Some("missing DPoP proof".into()),
274 jkt_expected: Some(expected_jkt.into()),
275 jkt_seen: None,
276 };
277 }
278 let parts: Vec<&str> = proof_jwt.split('.').collect();
279 if parts.len() != 3 {
280 return DpopProofVerification {
281 ok: false,
282 reason: Some("DPoP proof not a JWT".into()),
283 jkt_expected: Some(expected_jkt.into()),
284 jkt_seen: None,
285 };
286 }
287 let header_bytes = match URL_SAFE_NO_PAD.decode(parts[0]) {
288 Ok(b) => b,
289 Err(e) => {
290 return DpopProofVerification {
291 ok: false,
292 reason: Some(format!("DPoP header decode: {}", e)),
293 jkt_expected: Some(expected_jkt.into()),
294 jkt_seen: None,
295 }
296 }
297 };
298 let header: Value = match serde_json::from_slice(&header_bytes) {
299 Ok(v) => v,
300 Err(e) => {
301 return DpopProofVerification {
302 ok: false,
303 reason: Some(format!("DPoP header parse: {}", e)),
304 jkt_expected: Some(expected_jkt.into()),
305 jkt_seen: None,
306 }
307 }
308 };
309 if header.get("typ").and_then(|v| v.as_str()) != Some("dpop+jwt") {
310 return DpopProofVerification {
311 ok: false,
312 reason: Some(format!("DPoP typ {:?} is not dpop+jwt", header.get("typ"))),
313 jkt_expected: Some(expected_jkt.into()),
314 jkt_seen: None,
315 };
316 }
317 let jwk_value = match header.get("jwk") {
318 Some(v) => v,
319 None => {
320 return DpopProofVerification {
321 ok: false,
322 reason: Some("DPoP header missing jwk".into()),
323 jkt_expected: Some(expected_jkt.into()),
324 jkt_seen: None,
325 }
326 }
327 };
328 let jwk: Jwk = match serde_json::from_value(jwk_value.clone()) {
329 Ok(v) => v,
330 Err(e) => {
331 return DpopProofVerification {
332 ok: false,
333 reason: Some(format!("DPoP jwk parse: {}", e)),
334 jkt_expected: Some(expected_jkt.into()),
335 jkt_seen: None,
336 }
337 }
338 };
339 let jkt = match jwk_thumbprint(&jwk) {
340 Ok(s) => s,
341 Err(e) => {
342 return DpopProofVerification {
343 ok: false,
344 reason: Some(format!("DPoP thumbprint: {}", e)),
345 jkt_expected: Some(expected_jkt.into()),
346 jkt_seen: None,
347 }
348 }
349 };
350 if jkt != expected_jkt {
351 return DpopProofVerification {
352 ok: false,
353 reason: Some("jkt mismatch".into()),
354 jkt_expected: Some(expected_jkt.into()),
355 jkt_seen: Some(jkt),
356 };
357 }
358 let key = match decoding_key_for_jwk(&jwk) {
359 Ok(k) => k,
360 Err(e) => {
361 return DpopProofVerification {
362 ok: false,
363 reason: Some(format!("DPoP key build: {}", e)),
364 jkt_expected: Some(expected_jkt.into()),
365 jkt_seen: Some(jkt),
366 }
367 }
368 };
369 let alg_name = header
370 .get("alg")
371 .and_then(|v| v.as_str())
372 .unwrap_or("ES256");
373 let alg = match crate::bridge_oauth::parse_algorithm(alg_name) {
374 Ok(a) => a,
375 Err(e) => {
376 return DpopProofVerification {
377 ok: false,
378 reason: Some(format!("DPoP alg parse: {}", e)),
379 jkt_expected: Some(expected_jkt.into()),
380 jkt_seen: Some(jkt),
381 }
382 }
383 };
384 let mut validation = Validation::new(alg);
385 validation.required_spec_claims.clear();
386 validation.validate_exp = false;
387 validation.validate_aud = false;
388 validation.algorithms = vec![alg];
389 let payload = match decode::<HashMap<String, Value>>(proof_jwt, &key, &validation) {
390 Ok(d) => d.claims,
391 Err(e) => {
392 return DpopProofVerification {
393 ok: false,
394 reason: Some(format!("DPoP signature verify failed: {}", e)),
395 jkt_expected: Some(expected_jkt.into()),
396 jkt_seen: Some(jkt),
397 }
398 }
399 };
400 if payload.get("htm").and_then(|v| v.as_str()) != Some(htm) {
401 return DpopProofVerification {
402 ok: false,
403 reason: Some(format!(
404 "DPoP htm {:?} does not match expected {}",
405 payload.get("htm"),
406 htm
407 )),
408 jkt_expected: Some(expected_jkt.into()),
409 jkt_seen: Some(jkt),
410 };
411 }
412 if payload.get("htu").and_then(|v| v.as_str()) != Some(htu) {
413 return DpopProofVerification {
414 ok: false,
415 reason: Some(format!(
416 "DPoP htu {:?} does not match expected {}",
417 payload.get("htu"),
418 htu
419 )),
420 jkt_expected: Some(expected_jkt.into()),
421 jkt_seen: Some(jkt),
422 };
423 }
424 if let Some(expected_ath) = access_token_hash {
425 if payload.get("ath").and_then(|v| v.as_str()) != Some(expected_ath) {
426 return DpopProofVerification {
427 ok: false,
428 reason: Some("DPoP ath does not match expected access-token hash".into()),
429 jkt_expected: Some(expected_jkt.into()),
430 jkt_seen: Some(jkt),
431 };
432 }
433 }
434 if !payload.get("iat").map(|v| v.is_number()).unwrap_or(false) {
435 return DpopProofVerification {
436 ok: false,
437 reason: Some("DPoP missing iat".into()),
438 jkt_expected: Some(expected_jkt.into()),
439 jkt_seen: Some(jkt),
440 };
441 }
442 DpopProofVerification {
443 ok: true,
444 reason: None,
445 jkt_expected: Some(expected_jkt.into()),
446 jkt_seen: Some(jkt),
447 }
448 }
449}
450
451impl Bridge for GnapBridge {
452 fn bridge_id(&self) -> &str {
453 &self.cfg.bridge_id
454 }
455 fn kind(&self) -> BridgeKind {
456 BridgeKind::Gnap
457 }
458 fn trust_domain(&self) -> &str {
459 &self.cfg.trust_domain
460 }
461}
462
463fn decoding_key_for_jwk(jwk: &Jwk) -> Result<DecodingKey, BridgeError> {
464 match jwk.kty.as_str() {
465 "EC" => {
466 let x = jwk
467 .x
468 .as_ref()
469 .ok_or_else(|| BridgeError::InvalidInput("EC JWK missing x".into()))?;
470 let y = jwk
471 .y
472 .as_ref()
473 .ok_or_else(|| BridgeError::InvalidInput("EC JWK missing y".into()))?;
474 DecodingKey::from_ec_components(x, y)
475 .map_err(|e| BridgeError::InvalidInput(format!("bad EC components: {}", e)))
476 }
477 "RSA" => {
478 let n = jwk
479 .n
480 .as_ref()
481 .ok_or_else(|| BridgeError::InvalidInput("RSA JWK missing n".into()))?;
482 let e = jwk
483 .e
484 .as_ref()
485 .ok_or_else(|| BridgeError::InvalidInput("RSA JWK missing e".into()))?;
486 DecodingKey::from_rsa_components(n, e)
487 .map_err(|e| BridgeError::InvalidInput(format!("bad RSA components: {}", e)))
488 }
489 "OKP" => {
490 let x = jwk
491 .x
492 .as_ref()
493 .ok_or_else(|| BridgeError::InvalidInput("OKP JWK missing x".into()))?;
494 DecodingKey::from_ed_components(x)
495 .map_err(|e| BridgeError::InvalidInput(format!("bad OKP components: {}", e)))
496 }
497 other => Err(BridgeError::InvalidInput(format!(
498 "unsupported kty {}",
499 other
500 ))),
501 }
502}
503
504pub fn jwk_thumbprint(jwk: &Jwk) -> Result<String, BridgeError> {
508 let canonical = match jwk.kty.as_str() {
509 "EC" => {
510 let crv = jwk
511 .crv
512 .as_ref()
513 .ok_or_else(|| BridgeError::InvalidInput("EC JWK missing crv".into()))?;
514 let x = jwk
515 .x
516 .as_ref()
517 .ok_or_else(|| BridgeError::InvalidInput("EC JWK missing x".into()))?;
518 let y = jwk
519 .y
520 .as_ref()
521 .ok_or_else(|| BridgeError::InvalidInput("EC JWK missing y".into()))?;
522 format!(
523 "{{\"crv\":\"{}\",\"kty\":\"{}\",\"x\":\"{}\",\"y\":\"{}\"}}",
524 crv, jwk.kty, x, y
525 )
526 }
527 "OKP" => {
528 let crv = jwk
529 .crv
530 .as_ref()
531 .ok_or_else(|| BridgeError::InvalidInput("OKP JWK missing crv".into()))?;
532 let x = jwk
533 .x
534 .as_ref()
535 .ok_or_else(|| BridgeError::InvalidInput("OKP JWK missing x".into()))?;
536 format!(
537 "{{\"crv\":\"{}\",\"kty\":\"{}\",\"x\":\"{}\"}}",
538 crv, jwk.kty, x
539 )
540 }
541 "RSA" => {
542 let n = jwk
543 .n
544 .as_ref()
545 .ok_or_else(|| BridgeError::InvalidInput("RSA JWK missing n".into()))?;
546 let e = jwk
547 .e
548 .as_ref()
549 .ok_or_else(|| BridgeError::InvalidInput("RSA JWK missing e".into()))?;
550 format!(
551 "{{\"e\":\"{}\",\"kty\":\"{}\",\"n\":\"{}\"}}",
552 e, jwk.kty, n
553 )
554 }
555 other => {
556 return Err(BridgeError::Unsupported(format!(
557 "unsupported kty for thumbprint: {}",
558 other
559 )))
560 }
561 };
562 let digest: [u8; 32] = Sha256::digest(canonical.as_bytes()).into();
563 Ok(URL_SAFE_NO_PAD.encode(digest))
564}
565
566fn timestamp(t: u64) -> String {
567 let secs = t as i64;
568 let (year, month, day, hour, minute, second) = secs_to_ymdhms(secs);
569 format!(
570 "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z",
571 year, month, day, hour, minute, second
572 )
573}
574
575fn now_unix() -> u64 {
576 std::time::SystemTime::now()
577 .duration_since(std::time::UNIX_EPOCH)
578 .unwrap_or_default()
579 .as_secs()
580}
581
582fn secs_to_ymdhms(secs: i64) -> (i32, u32, u32, u32, u32, u32) {
583 let days = secs.div_euclid(86_400);
584 let time = secs.rem_euclid(86_400);
585 let hour = (time / 3600) as u32;
586 let minute = ((time % 3600) / 60) as u32;
587 let second = (time % 60) as u32;
588 let z = days + 719_468;
589 let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
590 let doe = (z - era * 146_097) as u64;
591 let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
592 let y = yoe as i64 + era * 400;
593 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
594 let mp = (5 * doy + 2) / 153;
595 let d = (doy - (153 * mp + 2) / 5 + 1) as u32;
596 let m = if mp < 10 {
597 (mp + 3) as u32
598 } else {
599 (mp - 9) as u32
600 };
601 let year = if m <= 2 { y + 1 } else { y };
602 (year as i32, m, d, hour, minute, second)
603}
604
605fn url_encode(s: &str) -> String {
606 let mut out = String::with_capacity(s.len());
607 for b in s.bytes() {
608 match b {
609 b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
610 out.push(b as char);
611 }
612 _ => out.push_str(&format!("%{:02X}", b)),
613 }
614 }
615 out
616}