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