Skip to main content

auths_sdk/
device.rs

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
59/// Links a new device to an existing identity by creating a signed attestation.
60///
61/// Args:
62/// * `config`: Device link parameters (identity alias, capabilities, etc.).
63/// * `ctx`: Runtime context providing storage adapters, key material, and passphrase provider.
64/// * `clock`: Clock provider for timestamp generation.
65///
66/// Usage:
67/// ```ignore
68/// let result = link_device(config, &ctx, &SystemClock)?;
69/// ```
70pub 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        &params,
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
105/// Revokes a device's attestation by creating a signed revocation record.
106///
107/// Args:
108/// * `device_did`: The DID of the device to revoke.
109/// * `identity_key_alias`: Keychain alias for the identity key that will sign the revocation.
110/// * `ctx`: Runtime context providing storage adapters, key material, and passphrase provider.
111/// * `note`: Optional reason for revocation.
112/// * `clock`: Clock provider for timestamp generation.
113///
114/// Usage:
115/// ```ignore
116/// revoke_device("did:key:z6Mk...", "my-identity", &ctx, Some("Lost laptop"), &clock)?;
117/// ```
118pub 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
153/// Extends the expiration of an existing device authorization by creating a new attestation.
154///
155/// Loads the latest attestation for the given device DID, verifies it is not revoked,
156/// then creates a new signed attestation with the extended expiry and persists it.
157/// Capabilities are preserved as empty (`vec![]`) — the extension renews the grant
158/// duration only, it does not change what the device is permitted to do.
159///
160/// Args:
161/// * `config`: Extension parameters (device DID, days, key aliases, registry path).
162/// * `ctx`: Runtime context providing storage adapters, key material, and passphrase provider.
163/// * `clock`: Clock provider for timestamp generation.
164///
165/// Usage:
166/// ```ignore
167/// let result = extend_device(config, &ctx, &SystemClock)?;
168/// ```
169pub 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        &params.identity_did,
304        &params.device_did,
305        &params.device_public_key,
306        params.payload.clone(),
307        &params.meta,
308        signer,
309        passphrase_provider,
310        Some(&params.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}