1use serde::{Deserialize, Serialize};
11
12use crate::bridges::{Bridge, BridgeError, BridgeKind};
13use crate::generated::{
14 ActorIdentity, ActorIdentity_IdentityVersion, ActorType, AuthorityRoot, AuthorityRoot_Kind,
15 PublicKey, PublicKey_Purpose, TrustLevel,
16};
17
18#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
19pub struct WebAuthnCredential {
20 pub credential_id: String,
21 pub public_key: String,
22 pub algorithm: String,
23 pub rp_id: String,
24 pub user_handle: String,
25 #[serde(skip_serializing_if = "Option::is_none", default)]
26 pub aaguid: Option<String>,
27 #[serde(skip_serializing_if = "Option::is_none", default)]
28 pub attestation_format: Option<String>,
29 #[serde(skip_serializing_if = "Option::is_none", default)]
30 pub valid_from: Option<String>,
31 #[serde(skip_serializing_if = "Option::is_none", default)]
32 pub valid_until: Option<String>,
33}
34
35#[derive(Clone, Debug, Default)]
36pub struct WebAuthnBridgeConfig {
37 pub bridge_id: String,
38 pub trust_domain: String,
39 pub rp_id: String,
40 pub allowed_algorithms: Option<Vec<String>>,
43}
44
45pub struct WebAuthnBridge {
46 cfg: WebAuthnBridgeConfig,
47}
48
49impl WebAuthnBridge {
50 pub fn new(cfg: WebAuthnBridgeConfig) -> Self {
51 WebAuthnBridge { cfg }
52 }
53
54 pub fn accept(&self, cred: &WebAuthnCredential) -> Result<ActorIdentity, BridgeError> {
58 if cred.public_key.is_empty() {
59 return Err(BridgeError::InvalidInput("missing public_key".into()));
60 }
61 if cred.rp_id.is_empty() {
62 return Err(BridgeError::InvalidInput("missing rp_id".into()));
63 }
64 if cred.user_handle.is_empty() {
65 return Err(BridgeError::InvalidInput("missing user_handle".into()));
66 }
67 if !self.cfg.rp_id.is_empty() && self.cfg.rp_id != cred.rp_id {
68 return Err(BridgeError::Rejected(format!(
69 "credential rp_id {} does not match bridge rp_id {}",
70 cred.rp_id, self.cfg.rp_id
71 )));
72 }
73 if let Some(allow) = &self.cfg.allowed_algorithms {
74 if !allow.iter().any(|a| a == &cred.algorithm) {
75 return Err(BridgeError::Rejected(format!(
76 "algorithm {} is not in the bridge's allow-list",
77 cred.algorithm
78 )));
79 }
80 }
81 let now = current_iso8601();
82 let actor_id = format!("tf:actor:human:{}/{}", cred.rp_id, slug(&cred.user_handle));
83 let identity = ActorIdentity {
84 identity_version: ActorIdentity_IdentityVersion::V1,
85 actor_id,
86 actor_type: ActorType::Human,
87 instance_id: None,
88 public_keys: vec![PublicKey {
89 key_id: cred.credential_id.clone(),
90 algorithm: cred.algorithm.clone(),
91 public_key: cred.public_key.clone(),
92 purpose: PublicKey_Purpose::Signing,
93 valid_from: cred.valid_from.clone(),
94 valid_until: cred.valid_until.clone(),
95 }],
96 trust_levels: vec![TrustLevel::T4],
97 authority_roots: vec![AuthorityRoot {
98 kind: AuthorityRoot_Kind::HardwareKey,
99 id: cred
100 .aaguid
101 .clone()
102 .unwrap_or_else(|| "(unknown-aaguid)".to_string()),
103 }],
104 attestations: None,
105 valid_from: cred.valid_from.clone().unwrap_or(now),
106 valid_until: cred.valid_until.clone(),
107 revocation_ref: None,
108 signature: None,
109 };
110 Ok(identity)
111 }
112
113 pub fn project(&self, identity: &ActorIdentity) -> Result<WebAuthnCredential, BridgeError> {
115 if !matches!(identity.actor_type, ActorType::Human) {
116 return Err(BridgeError::Unsupported(format!(
117 "WebAuthn bridge only reverses human actors, got {:?}",
118 identity.actor_type
119 )));
120 }
121 let hardware_root = identity
122 .authority_roots
123 .iter()
124 .find(|r| matches!(r.kind, AuthorityRoot_Kind::HardwareKey))
125 .ok_or_else(|| {
126 BridgeError::Rejected(
127 "identity's authority_roots does not include hardware-key".into(),
128 )
129 })?;
130 let key = identity
131 .public_keys
132 .first()
133 .ok_or_else(|| BridgeError::InvalidInput("identity has no public_keys".into()))?;
134 let (rp_id, user_handle) = parse_actor_uri(&identity.actor_id)?;
135 Ok(WebAuthnCredential {
136 credential_id: key.key_id.clone(),
137 public_key: key.public_key.clone(),
138 algorithm: key.algorithm.clone(),
139 rp_id,
140 user_handle,
141 aaguid: if hardware_root.id == "(unknown-aaguid)" {
142 None
143 } else {
144 Some(hardware_root.id.clone())
145 },
146 attestation_format: None,
147 valid_from: Some(identity.valid_from.clone()),
148 valid_until: identity.valid_until.clone(),
149 })
150 }
151}
152
153impl Bridge for WebAuthnBridge {
154 fn bridge_id(&self) -> &str {
155 &self.cfg.bridge_id
156 }
157 fn kind(&self) -> BridgeKind {
158 BridgeKind::Webauthn
159 }
160 fn trust_domain(&self) -> &str {
161 &self.cfg.trust_domain
162 }
163}
164
165fn slug(b64url: &str) -> String {
166 b64url
167 .trim_end_matches('=')
168 .replace('/', "_")
169 .replace('+', "-")
170}
171
172fn parse_actor_uri(uri: &str) -> Result<(String, String), BridgeError> {
173 let rest = uri
175 .strip_prefix("tf:actor:human:")
176 .ok_or_else(|| BridgeError::InvalidInput(format!("malformed actor URI: {}", uri)))?;
177 let slash = rest
178 .find('/')
179 .ok_or_else(|| BridgeError::InvalidInput(format!("malformed actor URI: {}", uri)))?;
180 Ok((rest[..slash].to_string(), rest[slash + 1..].to_string()))
181}
182
183fn current_iso8601() -> String {
184 let secs = std::time::SystemTime::now()
185 .duration_since(std::time::UNIX_EPOCH)
186 .unwrap_or_default()
187 .as_secs() as i64;
188 let (year, month, day, hour, minute, second) = civil_from_unix(secs);
189 format!(
190 "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z",
191 year, month, day, hour, minute, second
192 )
193}
194
195fn civil_from_unix(secs: i64) -> (i32, u32, u32, u32, u32, u32) {
196 let days = secs.div_euclid(86_400);
197 let time = secs.rem_euclid(86_400);
198 let hour = (time / 3600) as u32;
199 let minute = ((time % 3600) / 60) as u32;
200 let second = (time % 60) as u32;
201 let z = days + 719_468;
202 let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
203 let doe = (z - era * 146_097) as u64;
204 let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
205 let y = yoe as i64 + era * 400;
206 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
207 let mp = (5 * doy + 2) / 153;
208 let d = (doy - (153 * mp + 2) / 5 + 1) as u32;
209 let m = if mp < 10 {
210 (mp + 3) as u32
211 } else {
212 (mp - 9) as u32
213 };
214 let year = if m <= 2 { y + 1 } else { y };
215 (year as i32, m, d, hour, minute, second)
216}