1use std::convert::TryInto;
2use std::sync::Arc;
3
4use auths_core::ports::clock::ClockProvider;
5use auths_core::signing::{PassphraseProvider, SecureSigner, StorageSigner};
6use auths_core::storage::keychain::{IdentityDID, KeyAlias, KeyStorage};
7use auths_id::attestation::create::create_signed_attestation;
8use auths_id::attestation::export::AttestationSink;
9use auths_id::attestation::group::AttestationGroup;
10use auths_id::attestation::revoke::create_signed_revocation;
11use auths_id::storage::attestation::AttestationSource;
12use auths_id::storage::git_refs::AttestationMetadata;
13use auths_id::storage::identity::IdentityStorage;
14use auths_verifier::core::{Capability, Ed25519PublicKey, ResourceId};
15use auths_verifier::types::DeviceDID;
16use chrono::{DateTime, Utc};
17
18use crate::context::AuthsContext;
19use crate::error::{DeviceError, DeviceExtensionError};
20use crate::result::{DeviceExtensionResult, DeviceLinkResult};
21use crate::types::{DeviceExtensionConfig, DeviceLinkConfig};
22
23struct AttestationParams {
24 identity_did: IdentityDID,
25 device_did: DeviceDID,
26 device_public_key: Vec<u8>,
27 payload: Option<serde_json::Value>,
28 meta: AttestationMetadata,
29 capabilities: Vec<Capability>,
30 identity_alias: KeyAlias,
31 device_alias: Option<KeyAlias>,
32}
33
34fn build_attestation_params(
35 config: &DeviceLinkConfig,
36 identity_did: IdentityDID,
37 device_did: DeviceDID,
38 device_public_key: Vec<u8>,
39 now: DateTime<Utc>,
40) -> AttestationParams {
41 AttestationParams {
42 identity_did,
43 device_did,
44 device_public_key,
45 payload: config.payload.clone(),
46 meta: AttestationMetadata {
47 timestamp: Some(now),
48 expires_at: config
49 .expires_in_days
50 .map(|d| now + chrono::Duration::days(d as i64)),
51 note: config.note.clone(),
52 },
53 capabilities: config.capabilities.clone(),
54 identity_alias: config.identity_key_alias.clone(),
55 device_alias: config.device_key_alias.clone(),
56 }
57}
58
59pub fn link_device(
71 config: DeviceLinkConfig,
72 ctx: &AuthsContext,
73 clock: &dyn ClockProvider,
74) -> Result<DeviceLinkResult, DeviceError> {
75 let now = clock.now();
76 let identity = load_identity(ctx.identity_storage.as_ref())?;
77 let signer = StorageSigner::new(Arc::clone(&ctx.key_storage));
78 let (device_did, pk_bytes) = extract_device_key(
79 &config,
80 ctx.key_storage.as_ref(),
81 ctx.passphrase_provider.as_ref(),
82 )?;
83 let params = build_attestation_params(
84 &config,
85 identity.controller_did,
86 device_did.clone(),
87 pk_bytes,
88 now,
89 );
90 let attestation_rid = sign_and_persist_attestation(
91 now,
92 ¶ms,
93 &identity.storage_id,
94 &signer,
95 ctx.passphrase_provider.as_ref(),
96 ctx.attestation_sink.as_ref(),
97 )?;
98
99 Ok(DeviceLinkResult {
100 device_did,
101 attestation_id: ResourceId::new(attestation_rid),
102 })
103}
104
105pub fn revoke_device(
119 device_did: &str,
120 identity_key_alias: &KeyAlias,
121 ctx: &AuthsContext,
122 note: Option<String>,
123 clock: &dyn ClockProvider,
124) -> Result<(), DeviceError> {
125 let now = clock.now();
126 let identity = load_identity(ctx.identity_storage.as_ref())?;
127 let device_pk = find_device_public_key(ctx.attestation_source.as_ref(), device_did)?;
128 let signer = StorageSigner::new(Arc::clone(&ctx.key_storage));
129
130 let target_did = DeviceDID::from_ed25519(device_pk.as_bytes());
131
132 let revocation = create_signed_revocation(
133 &identity.storage_id,
134 &identity.controller_did,
135 &target_did,
136 device_pk.as_bytes(),
137 note,
138 None,
139 now,
140 &signer,
141 ctx.passphrase_provider.as_ref(),
142 identity_key_alias,
143 )
144 .map_err(|e| DeviceError::AttestationError(format!("revocation signing failed: {e}")))?;
145
146 ctx.attestation_sink
147 .export(&auths_verifier::VerifiedAttestation::dangerous_from_unchecked(revocation))
148 .map_err(|e| DeviceError::StorageError(e.into()))?;
149
150 Ok(())
151}
152
153pub fn extend_device(
170 config: DeviceExtensionConfig,
171 ctx: &AuthsContext,
172 clock: &dyn ClockProvider,
173) -> Result<DeviceExtensionResult, DeviceExtensionError> {
174 let signer = StorageSigner::new(Arc::clone(&ctx.key_storage));
175
176 let identity = load_identity(ctx.identity_storage.as_ref())
177 .map_err(|_| DeviceExtensionError::IdentityNotFound)?;
178
179 let group = AttestationGroup::from_list(
180 ctx.attestation_source
181 .load_all_attestations()
182 .map_err(|e| DeviceExtensionError::StorageError(e.into()))?,
183 );
184
185 let device_did_obj = DeviceDID(config.device_did.clone());
186 let latest =
187 group
188 .latest(&device_did_obj)
189 .ok_or_else(|| DeviceExtensionError::NoAttestationFound {
190 device_did: config.device_did.clone(),
191 })?;
192
193 if latest.is_revoked() {
194 return Err(DeviceExtensionError::AlreadyRevoked {
195 device_did: config.device_did.clone(),
196 });
197 }
198
199 let previous_expires_at = latest.expires_at;
200 let now = clock.now();
201 let new_expires_at = now + chrono::Duration::days(config.days as i64);
202
203 let meta = AttestationMetadata {
204 note: latest.note.clone(),
205 timestamp: Some(now),
206 expires_at: Some(new_expires_at),
207 };
208
209 let extended = create_signed_attestation(
210 now,
211 &identity.storage_id,
212 &identity.controller_did,
213 &device_did_obj,
214 latest.device_public_key.as_bytes(),
215 latest.payload.clone(),
216 &meta,
217 &signer,
218 ctx.passphrase_provider.as_ref(),
219 Some(&config.identity_key_alias),
220 config.device_key_alias.as_ref(),
221 vec![],
222 None,
223 None,
224 )
225 .map_err(|e| DeviceExtensionError::AttestationFailed(e.to_string()))?;
226
227 ctx.attestation_sink
228 .export(&auths_verifier::VerifiedAttestation::dangerous_from_unchecked(extended.clone()))
229 .map_err(|e| DeviceExtensionError::StorageError(e.into()))?;
230
231 ctx.attestation_sink.sync_index(&extended);
232
233 Ok(DeviceExtensionResult {
234 device_did: DeviceDID::new(config.device_did),
235 new_expires_at,
236 previous_expires_at,
237 })
238}
239
240struct LoadedIdentity {
241 controller_did: IdentityDID,
242 storage_id: String,
243}
244
245fn load_identity(identity_storage: &dyn IdentityStorage) -> Result<LoadedIdentity, DeviceError> {
246 let managed = identity_storage
247 .load_identity()
248 .map_err(|e| DeviceError::IdentityNotFound {
249 did: format!("identity load failed: {e}"),
250 })?;
251 Ok(LoadedIdentity {
252 controller_did: managed.controller_did,
253 storage_id: managed.storage_id,
254 })
255}
256
257fn extract_device_key(
258 config: &DeviceLinkConfig,
259 keychain: &(dyn KeyStorage + Send + Sync),
260 passphrase_provider: &dyn PassphraseProvider,
261) -> Result<(DeviceDID, Vec<u8>), DeviceError> {
262 let alias = config
263 .device_key_alias
264 .as_ref()
265 .unwrap_or(&config.identity_key_alias);
266
267 let pk_bytes = auths_core::storage::keychain::extract_public_key_bytes(
268 keychain,
269 alias,
270 passphrase_provider,
271 )
272 .map_err(DeviceError::CryptoError)?;
273
274 let device_did = DeviceDID::from_ed25519(pk_bytes.as_slice().try_into().map_err(|_| {
275 DeviceError::CryptoError(auths_core::AgentError::InvalidInput(
276 "public key is not 32 bytes".into(),
277 ))
278 })?);
279
280 if let Some(ref expected) = config.device_did
281 && expected != &device_did.to_string()
282 {
283 return Err(DeviceError::AttestationError(format!(
284 "--device-did {} does not match key-derived DID {}",
285 expected, device_did
286 )));
287 }
288
289 Ok((device_did, pk_bytes))
290}
291
292fn sign_and_persist_attestation(
293 now: DateTime<Utc>,
294 params: &AttestationParams,
295 rid: &str,
296 signer: &dyn SecureSigner,
297 passphrase_provider: &dyn PassphraseProvider,
298 attestation_sink: &dyn AttestationSink,
299) -> Result<String, DeviceError> {
300 let attestation = create_signed_attestation(
301 now,
302 rid,
303 ¶ms.identity_did,
304 ¶ms.device_did,
305 ¶ms.device_public_key,
306 params.payload.clone(),
307 ¶ms.meta,
308 signer,
309 passphrase_provider,
310 Some(¶ms.identity_alias),
311 params.device_alias.as_ref(),
312 params.capabilities.clone(),
313 None,
314 None,
315 )
316 .map_err(|e| DeviceError::AttestationError(format!("attestation creation failed: {e}")))?;
317
318 let attestation_rid = attestation.rid.to_string();
319
320 attestation_sink
321 .export(&auths_verifier::VerifiedAttestation::dangerous_from_unchecked(attestation))
322 .map_err(|e| DeviceError::StorageError(e.into()))?;
323
324 Ok(attestation_rid)
325}
326
327fn find_device_public_key(
328 attestation_source: &dyn AttestationSource,
329 device_did: &str,
330) -> Result<Ed25519PublicKey, DeviceError> {
331 let attestations = attestation_source
332 .load_all_attestations()
333 .map_err(|e| DeviceError::StorageError(e.into()))?;
334
335 for att in &attestations {
336 if att.subject.as_str() == device_did {
337 return Ok(att.device_public_key);
338 }
339 }
340
341 Err(DeviceError::DeviceNotFound {
342 did: device_did.to_string(),
343 })
344}