use std::sync::Arc;
use auths_core::ports::clock::ClockProvider;
use auths_core::signing::{PassphraseProvider, SecureSigner, StorageSigner};
use auths_core::storage::keychain::{IdentityDID, KeyAlias, KeyStorage};
use auths_id::attestation::create::create_signed_attestation;
use auths_id::attestation::group::AttestationGroup;
use auths_id::attestation::revoke::create_signed_revocation;
use auths_id::keri::{anchor_and_persist_via_backend, parse_did_keri};
use auths_id::storage::attestation::AttestationSource;
use auths_id::storage::git_refs::AttestationMetadata;
use auths_id::storage::identity::IdentityStorage;
use auths_verifier::core::ResourceId;
use auths_verifier::types::CanonicalDid;
use chrono::{DateTime, Utc};
use crate::context::AuthsContext;
use crate::domains::device::error::{DeviceError, DeviceExtensionError};
use crate::domains::device::types::{
DeviceExtensionConfig, DeviceExtensionResult, DeviceLinkConfig, DeviceLinkResult,
};
struct AttestationParams {
identity_did: IdentityDID,
device_did: CanonicalDid,
device_public_key: auths_verifier::DevicePublicKey,
payload: Option<serde_json::Value>,
meta: AttestationMetadata,
identity_alias: KeyAlias,
device_alias: Option<KeyAlias>,
}
fn build_attestation_params(
config: &DeviceLinkConfig,
identity_did: IdentityDID,
device_did: CanonicalDid,
device_public_key: auths_verifier::DevicePublicKey,
now: DateTime<Utc>,
) -> AttestationParams {
AttestationParams {
identity_did,
device_did,
device_public_key,
payload: config.payload.clone(),
meta: AttestationMetadata {
timestamp: Some(now),
expires_at: config
.expires_in
.map(|s| now + chrono::Duration::seconds(s as i64)),
note: config.note.clone(),
},
identity_alias: config.identity_key_alias.clone(),
device_alias: config.device_key_alias.clone(),
}
}
pub fn link_device(
config: DeviceLinkConfig,
ctx: &AuthsContext,
clock: &dyn ClockProvider,
) -> Result<DeviceLinkResult, DeviceError> {
let now = clock.now();
let identity = load_identity(ctx.identity_storage.as_ref())?;
let prefix = parse_did_keri(identity.controller_did.as_str()).map_err(|e| {
DeviceError::AnchorError(auths_id::keri::AnchorError::InvalidDid(e.to_string()))
})?;
let signer = StorageSigner::new(Arc::clone(&ctx.key_storage));
let (device_did, device_pk) = extract_device_key(
&config,
ctx.key_storage.as_ref(),
ctx.passphrase_provider.as_ref(),
)?;
let params = build_attestation_params(
&config,
identity.controller_did,
device_did.clone(),
device_pk,
now,
);
let attestation = sign_attestation(
now,
¶ms,
&identity.storage_id,
&signer,
ctx.passphrase_provider.as_ref(),
)?;
let attestation_rid = attestation.rid.to_string();
let mut batch = auths_id::storage::registry::backend::AtomicWriteBatch::new();
batch.stage_attestation(attestation.clone());
anchor_and_persist_via_backend(
ctx.registry.as_ref(),
&signer,
&config.identity_key_alias,
ctx.passphrase_provider.as_ref(),
&prefix,
&attestation,
&mut batch,
&ctx.witness_params(),
now,
)?;
Ok(DeviceLinkResult {
device_did,
attestation_id: ResourceId::new(attestation_rid),
})
}
pub fn revoke_device(
subject: &str,
identity_key_alias: &KeyAlias,
ctx: &AuthsContext,
note: Option<String>,
clock: &dyn ClockProvider,
) -> Result<(), DeviceError> {
let now = clock.now();
let identity = load_identity(ctx.identity_storage.as_ref())?;
let prefix = parse_did_keri(identity.controller_did.as_str()).map_err(|e| {
DeviceError::AnchorError(auths_id::keri::AnchorError::InvalidDid(e.to_string()))
})?;
let device_pk = find_device_public_key(ctx.attestation_source.as_ref(), subject)?;
let signer = StorageSigner::new(Arc::clone(&ctx.key_storage));
let target_did = CanonicalDid::from_public_key_did_key(device_pk.as_bytes(), device_pk.curve());
let revocation = create_signed_revocation(
auths_id::attestation::revoke::RevocationInput {
rid: &identity.storage_id,
identity_did: &identity.controller_did,
subject: &target_did,
device_public_key: device_pk.as_bytes(),
device_curve: device_pk.curve(),
note,
payload: None,
timestamp: now,
identity_alias: identity_key_alias,
},
&signer,
ctx.passphrase_provider.as_ref(),
)
.map_err(DeviceError::AttestationError)?;
let mut batch = auths_id::storage::registry::backend::AtomicWriteBatch::new();
batch.stage_attestation(revocation.clone());
anchor_and_persist_via_backend(
ctx.registry.as_ref(),
&signer,
identity_key_alias,
ctx.passphrase_provider.as_ref(),
&prefix,
&revocation,
&mut batch,
&ctx.witness_params(),
now,
)?;
Ok(())
}
pub fn extend_device(
config: DeviceExtensionConfig,
ctx: &AuthsContext,
clock: &dyn ClockProvider,
) -> Result<DeviceExtensionResult, DeviceExtensionError> {
let signer = StorageSigner::new(Arc::clone(&ctx.key_storage));
let identity = load_identity(ctx.identity_storage.as_ref())
.map_err(|_| DeviceExtensionError::IdentityNotFound)?;
let group = AttestationGroup::from_list(
ctx.attestation_source
.load_all_attestations()
.map_err(|e| DeviceExtensionError::StorageError(e.into()))?,
);
#[allow(clippy::disallowed_methods)]
let device_did_obj = CanonicalDid::new_unchecked(config.device_did.clone());
let latest =
group
.latest(&device_did_obj)
.ok_or_else(|| DeviceExtensionError::NoAttestationFound {
device_did: config.device_did.clone(),
})?;
if latest.is_revoked() {
return Err(DeviceExtensionError::AlreadyRevoked {
device_did: config.device_did.clone(),
});
}
let previous_expires_at = latest.expires_at;
let now = clock.now();
let new_expires_at = now + chrono::Duration::seconds(config.expires_in as i64);
let meta = AttestationMetadata {
note: latest.note.clone(),
timestamp: Some(now),
expires_at: Some(new_expires_at),
};
let extended = create_signed_attestation(
now,
auths_id::attestation::create::AttestationInput {
rid: &identity.storage_id,
identity_did: &identity.controller_did,
subject: &device_did_obj,
device_public_key: latest.device_public_key.as_bytes(),
device_curve: latest.device_public_key.curve(),
payload: latest.payload.clone(),
meta: &meta,
identity_alias: Some(&config.identity_key_alias),
device_alias: config.device_key_alias.as_ref(),
delegated_by: None,
commit_sha: None,
signer_type: None,
},
&signer,
ctx.passphrase_provider.as_ref(),
)
.map_err(DeviceExtensionError::AttestationFailed)?;
if let Ok(prefix) = parse_did_keri(identity.controller_did.as_str()) {
let signer = StorageSigner::new(Arc::clone(&ctx.key_storage));
let mut batch = auths_id::storage::registry::backend::AtomicWriteBatch::new();
batch.stage_attestation(extended.clone());
anchor_and_persist_via_backend(
ctx.registry.as_ref(),
&signer,
&config.identity_key_alias,
ctx.passphrase_provider.as_ref(),
&prefix,
&extended,
&mut batch,
&ctx.witness_params(),
now,
)?;
}
Ok(DeviceExtensionResult {
#[allow(clippy::disallowed_methods)] device_did: CanonicalDid::new_unchecked(config.device_did),
new_expires_at,
previous_expires_at,
})
}
struct LoadedIdentity {
controller_did: IdentityDID,
storage_id: String,
}
fn load_identity(identity_storage: &dyn IdentityStorage) -> Result<LoadedIdentity, DeviceError> {
let managed = identity_storage
.load_identity()
.map_err(|e| DeviceError::IdentityNotFound {
did: format!("identity load failed: {e}"),
})?;
Ok(LoadedIdentity {
controller_did: managed.controller_did,
storage_id: managed.storage_id,
})
}
fn extract_device_key(
config: &DeviceLinkConfig,
keychain: &(dyn KeyStorage + Send + Sync),
passphrase_provider: &dyn PassphraseProvider,
) -> Result<(CanonicalDid, auths_verifier::DevicePublicKey), DeviceError> {
let alias = config
.device_key_alias
.as_ref()
.unwrap_or(&config.identity_key_alias);
let (pk_bytes, curve) = auths_core::storage::keychain::extract_public_key_bytes(
keychain,
alias,
passphrase_provider,
)
.map_err(DeviceError::CryptoError)?;
let device_pk = auths_verifier::DevicePublicKey::try_new(curve, &pk_bytes).map_err(|e| {
DeviceError::CryptoError(auths_core::AgentError::InvalidInput(e.to_string()))
})?;
let device_did = CanonicalDid::from_public_key_did_key(&pk_bytes, curve);
if let Some(ref expected) = config.device_did
&& expected != &device_did.to_string()
{
return Err(DeviceError::DeviceDidMismatch {
expected: expected.clone(),
actual: device_did.to_string(),
});
}
Ok((device_did, device_pk))
}
fn sign_attestation(
now: DateTime<Utc>,
params: &AttestationParams,
rid: &str,
signer: &dyn SecureSigner,
passphrase_provider: &dyn PassphraseProvider,
) -> Result<auths_verifier::core::Attestation, DeviceError> {
create_signed_attestation(
now,
auths_id::attestation::create::AttestationInput {
rid,
identity_did: ¶ms.identity_did,
subject: ¶ms.device_did,
device_public_key: params.device_public_key.as_bytes(),
device_curve: params.device_public_key.curve(),
payload: params.payload.clone(),
meta: ¶ms.meta,
identity_alias: Some(¶ms.identity_alias),
device_alias: params.device_alias.as_ref(),
delegated_by: None,
commit_sha: None,
signer_type: None,
},
signer,
passphrase_provider,
)
.map_err(DeviceError::AttestationError)
}
fn find_device_public_key(
attestation_source: &dyn AttestationSource,
subject: &str,
) -> Result<auths_verifier::DevicePublicKey, DeviceError> {
let attestations = attestation_source
.load_all_attestations()
.map_err(|e| DeviceError::StorageError(e.into()))?;
for att in &attestations {
if att.subject.as_str() == subject {
return Ok(att.device_public_key.clone());
}
}
Err(DeviceError::DeviceNotFound {
did: subject.to_string(),
})
}