use chrono::{DateTime, Utc};
use auths_core::ports::namespace::{
Ecosystem, NamespaceOwnershipProof, NamespaceVerifier, NamespaceVerifyError, PackageName,
PlatformContext, VerificationChallenge,
};
use auths_core::signing::{PassphraseProvider, SecureSigner};
use auths_core::storage::keychain::KeyAlias;
use auths_transparency::entry::{EntryBody, EntryContent, EntryType};
use auths_verifier::CanonicalDid;
use auths_verifier::types::IdentityDID;
use base64::Engine;
use base64::engine::general_purpose::STANDARD as BASE64;
use thiserror::Error;
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum NamespaceError {
#[error("namespace '{ecosystem}/{package_name}' is already claimed")]
AlreadyClaimed {
ecosystem: String,
package_name: String,
},
#[error("namespace '{ecosystem}/{package_name}' not found")]
NotFound {
ecosystem: String,
package_name: String,
},
#[error("not authorized to manage namespace '{ecosystem}/{package_name}'")]
Unauthorized {
ecosystem: String,
package_name: String,
},
#[error("invalid ecosystem: {0}")]
InvalidEcosystem(String),
#[error("invalid package name: {0}")]
InvalidPackageName(String),
#[error("network error: {0}")]
NetworkError(String),
#[error("signing error: {0}")]
SigningError(String),
#[error("serialization error: {0}")]
SerializationError(String),
#[error("verification failed: {0}")]
VerificationFailed(#[from] NamespaceVerifyError),
}
pub struct DelegateNamespaceCommand {
pub ecosystem: String,
pub package_name: String,
pub delegate_did: String,
pub registry_url: String,
}
pub struct TransferNamespaceCommand {
pub ecosystem: String,
pub package_name: String,
pub new_owner_did: String,
pub registry_url: String,
}
pub struct NamespaceClaimResult {
pub ecosystem: String,
pub package_name: String,
pub owner_did: String,
pub log_sequence: u128,
}
pub struct NamespaceInfo {
pub ecosystem: String,
pub package_name: String,
pub owner_did: String,
pub delegates: Vec<String>,
}
pub struct SignedEntry {
pub content: serde_json::Value,
pub actor_sig: String,
}
impl SignedEntry {
pub fn to_request_body(&self) -> serde_json::Value {
serde_json::json!({
"content": self.content,
"actor_sig": self.actor_sig,
})
}
}
fn validate_ecosystem(ecosystem: &str) -> Result<(), NamespaceError> {
if ecosystem.is_empty() {
return Err(NamespaceError::InvalidEcosystem(
"ecosystem must not be empty".into(),
));
}
Ok(())
}
fn validate_package_name(package_name: &str) -> Result<(), NamespaceError> {
if package_name.is_empty() {
return Err(NamespaceError::InvalidPackageName(
"package name must not be empty".into(),
));
}
Ok(())
}
fn build_and_sign_entry(
content: &EntryContent,
signer: &dyn SecureSigner,
passphrase_provider: &dyn PassphraseProvider,
signer_alias: &KeyAlias,
) -> Result<SignedEntry, NamespaceError> {
let canonical_bytes = content
.canonicalize()
.map_err(|e| NamespaceError::SerializationError(e.to_string()))?;
let sig_bytes = signer
.sign_with_alias(signer_alias, passphrase_provider, &canonical_bytes)
.map_err(|e| NamespaceError::SigningError(e.to_string()))?;
let content_value = serde_json::to_value(content)
.map_err(|e| NamespaceError::SerializationError(e.to_string()))?;
Ok(SignedEntry {
content: content_value,
actor_sig: BASE64.encode(&sig_bytes),
})
}
pub fn sign_namespace_delegate(
cmd: &DelegateNamespaceCommand,
actor_did: &IdentityDID,
signer: &dyn SecureSigner,
passphrase_provider: &dyn PassphraseProvider,
signer_alias: &KeyAlias,
) -> Result<SignedEntry, NamespaceError> {
validate_ecosystem(&cmd.ecosystem)?;
validate_package_name(&cmd.package_name)?;
#[allow(clippy::disallowed_methods)]
let delegate_identity = IdentityDID::new_unchecked(&cmd.delegate_did);
#[allow(clippy::disallowed_methods)]
let canonical_actor = CanonicalDid::new_unchecked(actor_did.as_str());
let content = EntryContent {
entry_type: EntryType::NamespaceDelegate,
body: EntryBody::NamespaceDelegate {
ecosystem: cmd.ecosystem.clone(),
package_name: cmd.package_name.clone(),
delegate_did: delegate_identity,
},
actor_did: canonical_actor,
};
build_and_sign_entry(&content, signer, passphrase_provider, signer_alias)
}
pub fn sign_namespace_transfer(
cmd: &TransferNamespaceCommand,
actor_did: &IdentityDID,
signer: &dyn SecureSigner,
passphrase_provider: &dyn PassphraseProvider,
signer_alias: &KeyAlias,
) -> Result<SignedEntry, NamespaceError> {
validate_ecosystem(&cmd.ecosystem)?;
validate_package_name(&cmd.package_name)?;
#[allow(clippy::disallowed_methods)]
let new_owner_identity = IdentityDID::new_unchecked(&cmd.new_owner_did);
#[allow(clippy::disallowed_methods)]
let canonical_actor = CanonicalDid::new_unchecked(actor_did.as_str());
let content = EntryContent {
entry_type: EntryType::NamespaceTransfer,
body: EntryBody::NamespaceTransfer {
ecosystem: cmd.ecosystem.clone(),
package_name: cmd.package_name.clone(),
new_owner_did: new_owner_identity,
},
actor_did: canonical_actor,
};
build_and_sign_entry(&content, signer, passphrase_provider, signer_alias)
}
pub fn parse_claim_response(
ecosystem: &str,
package_name: &str,
owner_did: &str,
response: &serde_json::Value,
) -> NamespaceClaimResult {
let log_sequence: u128 = response
.get("sequence")
.and_then(|v| v.as_u64())
.map(u128::from)
.unwrap_or(0);
NamespaceClaimResult {
ecosystem: ecosystem.to_string(),
package_name: package_name.to_string(),
owner_did: owner_did.to_string(),
log_sequence,
}
}
pub fn parse_lookup_response(
ecosystem: &str,
package_name: &str,
body: &serde_json::Value,
) -> NamespaceInfo {
let owner_did = body
.get("owner_did")
.and_then(|v| v.as_str())
.unwrap_or_default()
.to_string();
let delegates = body
.get("delegates")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect()
})
.unwrap_or_default();
NamespaceInfo {
ecosystem: ecosystem.to_string(),
package_name: package_name.to_string(),
owner_did,
delegates,
}
}
pub struct NamespaceVerificationSession {
pub challenge: VerificationChallenge,
pub ecosystem: Ecosystem,
pub package_name: PackageName,
pub controller_did: CanonicalDid,
pub platform: PlatformContext,
}
impl NamespaceVerificationSession {
pub async fn complete_ref(
&self,
now: DateTime<Utc>,
verifier: &dyn NamespaceVerifier,
signer: &dyn SecureSigner,
passphrase_provider: &dyn PassphraseProvider,
signer_alias: &KeyAlias,
) -> Result<VerifiedClaimResult, NamespaceError> {
let proof = verifier
.verify(
now,
&self.package_name,
&self.controller_did,
&self.platform,
&self.challenge,
)
.await?;
let content = EntryContent {
entry_type: EntryType::NamespaceClaim,
body: EntryBody::NamespaceClaim {
ecosystem: self.ecosystem.as_str().to_string(),
package_name: self.package_name.as_str().to_string(),
proof_url: proof.proof_url.to_string(),
verification_method: format!("{:?}", proof.method),
},
actor_did: self.controller_did.clone(),
};
let signed = build_and_sign_entry(&content, signer, passphrase_provider, signer_alias)?;
Ok(VerifiedClaimResult {
signed_entry: signed,
proof,
})
}
}
pub struct VerifiedClaimResult {
pub signed_entry: SignedEntry,
pub proof: NamespaceOwnershipProof,
}
pub async fn initiate_namespace_claim(
now: DateTime<Utc>,
verifier: &dyn NamespaceVerifier,
ecosystem: Ecosystem,
package_name: PackageName,
controller_did: CanonicalDid,
platform: PlatformContext,
) -> Result<NamespaceVerificationSession, NamespaceError> {
let challenge = verifier
.initiate(now, &package_name, &controller_did, &platform)
.await?;
Ok(NamespaceVerificationSession {
challenge,
ecosystem,
package_name,
controller_did,
platform,
})
}