diff --git a/clippy.toml b/clippy.toml index abefbeaf..77f3da68 100644 --- a/clippy.toml +++ b/clippy.toml @@ -3,6 +3,13 @@ disallowed-methods = [ { path = "std::time::SystemTime::now", reason = "inject ClockProvider instead of calling SystemTime::now() directly", allow-invalid = true }, { path = "std::env::var", reason = "use EnvironmentConfig abstraction instead of reading env vars directly", allow-invalid = true }, { path = "uuid::Uuid::new_v4", reason = "Use UuidProvider::new_id() instead. Inject SystemUuidProvider in production and DeterministicUuidProvider in tests." }, + + # === DID/newtype construction: prefer parse() for external input === + { path = "auths_verifier::types::IdentityDID::new_unchecked", reason = "Use IdentityDID::parse() for external input. Use #[allow(clippy::disallowed_methods)] with INVARIANT comment for proven-safe paths.", allow-invalid = true }, + { path = "auths_verifier::types::DeviceDID::new_unchecked", reason = "Use DeviceDID::parse() for external input. Use #[allow(clippy::disallowed_methods)] with INVARIANT comment for proven-safe paths.", allow-invalid = true }, + { path = "auths_verifier::types::CanonicalDid::new_unchecked", reason = "Use CanonicalDid::parse() for external input. Use #[allow(clippy::disallowed_methods)] with INVARIANT comment for proven-safe paths.", allow-invalid = true }, + { path = "auths_verifier::core::CommitOid::new_unchecked", reason = "Use CommitOid::parse() for external input. Use #[allow(clippy::disallowed_methods)] with INVARIANT comment for proven-safe paths.", allow-invalid = true }, + { path = "auths_verifier::core::PublicKeyHex::new_unchecked", reason = "Use PublicKeyHex::parse() for external input. Use #[allow(clippy::disallowed_methods)] with INVARIANT comment for proven-safe paths.", allow-invalid = true }, ] allow-unwrap-in-tests = true allow-expect-in-tests = true diff --git a/crates/auths-cli/clippy.toml b/crates/auths-cli/clippy.toml index 8d799ba9..2e11bd88 100644 --- a/crates/auths-cli/clippy.toml +++ b/crates/auths-cli/clippy.toml @@ -12,7 +12,10 @@ disallowed-methods = [ { path = "std::env::var", reason = "use EnvironmentConfig abstraction instead of reading env vars directly", allow-invalid = true }, { path = "uuid::Uuid::new_v4", reason = "Use UuidProvider::new_id() instead. Inject SystemUuidProvider in production and DeterministicUuidProvider in tests." }, - # === DID construction: prefer parse() for user input === - { path = "auths_verifier::types::DeviceDID::new_unchecked", reason = "CLI should use DeviceDID::parse() for user input. Use #[allow] with INVARIANT comment for internally-derived values.", allow-invalid = true }, - { path = "auths_verifier::types::IdentityDID::new_unchecked", reason = "CLI should use IdentityDID::parse() for user input. Use #[allow] with INVARIANT comment for internally-derived values.", allow-invalid = true }, + # === DID/newtype construction: prefer parse() for external input === + { path = "auths_verifier::types::IdentityDID::new_unchecked", reason = "Use IdentityDID::parse() for external input. Use #[allow(clippy::disallowed_methods)] with INVARIANT comment for proven-safe paths.", allow-invalid = true }, + { path = "auths_verifier::types::DeviceDID::new_unchecked", reason = "Use DeviceDID::parse() for external input. Use #[allow(clippy::disallowed_methods)] with INVARIANT comment for proven-safe paths.", allow-invalid = true }, + { path = "auths_verifier::types::CanonicalDid::new_unchecked", reason = "Use CanonicalDid::parse() for external input. Use #[allow(clippy::disallowed_methods)] with INVARIANT comment for proven-safe paths.", allow-invalid = true }, + { path = "auths_verifier::core::CommitOid::new_unchecked", reason = "Use CommitOid::parse() for external input. Use #[allow(clippy::disallowed_methods)] with INVARIANT comment for proven-safe paths.", allow-invalid = true }, + { path = "auths_verifier::core::PublicKeyHex::new_unchecked", reason = "Use PublicKeyHex::parse() for external input. Use #[allow(clippy::disallowed_methods)] with INVARIANT comment for proven-safe paths.", allow-invalid = true }, ] diff --git a/crates/auths-cli/src/bin/sign.rs b/crates/auths-cli/src/bin/sign.rs index bc232067..0c784597 100644 --- a/crates/auths-cli/src/bin/sign.rs +++ b/crates/auths-cli/src/bin/sign.rs @@ -87,6 +87,28 @@ struct Args { buffer_file: Option, } +fn validate_verify_option(opt: &str) -> Result<()> { + match opt { + "print-pubkey" => return Ok(()), + "hashalg=sha256" | "hashalg=sha512" => return Ok(()), + _ => {} + } + + if let Some(value) = opt.strip_prefix("verify-time=") + && !value.is_empty() + && value.len() <= 14 + && value.bytes().all(|b| b.is_ascii_digit()) + { + return Ok(()); + } + + bail!( + "disallowed verify option '-O {opt}'\n \ + Only these -O options are permitted: verify-time=, print-pubkey, hashalg=sha256, hashalg=sha512\n \ + [AUTHS-E0031]" + ); +} + fn parse_key_identifier(key_file: &str) -> Result { if let Some(alias) = key_file.strip_prefix("auths:") { if alias.is_empty() { @@ -187,6 +209,7 @@ fn run_verify(args: &Args) -> Result<()> { ]); cmd.arg(sig_file); for opt in &args.verify_options { + validate_verify_option(opt)?; cmd.arg("-O").arg(opt); } let status = cmd @@ -219,6 +242,7 @@ fn run_delegate_to_ssh_keygen(args: &Args) -> Result<()> { cmd.arg("-s").arg(sig); } for opt in &args.verify_options { + validate_verify_option(opt)?; cmd.arg("-O").arg(opt); } let status = cmd @@ -484,4 +508,41 @@ mod tests { let result = extract_seed_from_pkcs8(&bad_input); assert!(result.is_err(), "must reject non-PKCS#8 input"); } + + #[test] + fn test_validate_verify_option_valid() { + assert!(validate_verify_option("verify-time=1700000000").is_ok()); + assert!(validate_verify_option("verify-time=20260218012319").is_ok()); + assert!(validate_verify_option("verify-time=1").is_ok()); + assert!(validate_verify_option("print-pubkey").is_ok()); + assert!(validate_verify_option("hashalg=sha256").is_ok()); + assert!(validate_verify_option("hashalg=sha512").is_ok()); + } + + #[test] + fn test_validate_verify_option_invalid() { + assert!(validate_verify_option("no-touch-required").is_err()); + assert!(validate_verify_option("foo=bar").is_err()); + assert!(validate_verify_option("random-option").is_err()); + assert!(validate_verify_option("").is_err()); + } + + #[test] + fn test_validate_verify_option_edge_cases() { + assert!(validate_verify_option("verify-time=").is_err()); + assert!(validate_verify_option("verify-time=abc").is_err()); + assert!(validate_verify_option("VERIFY-TIME=123").is_err()); + assert!(validate_verify_option("hashalg=sha384").is_err()); + assert!(validate_verify_option("verify-time=123=456").is_err()); + assert!(validate_verify_option(" verify-time=123").is_err()); + assert!(validate_verify_option("verify-time=999999999999999").is_err()); + } + + #[test] + fn test_validate_verify_option_injection_attempts() { + assert!(validate_verify_option("-D /tmp/evil.so").is_err()); + assert!(validate_verify_option("--help").is_err()); + assert!(validate_verify_option("-t rsa").is_err()); + assert!(validate_verify_option("-w /tmp/fido.so").is_err()); + } } diff --git a/crates/auths-cli/src/commands/id/identity.rs b/crates/auths-cli/src/commands/id/identity.rs index 9c34f9ca..0f9ca2ca 100644 --- a/crates/auths-cli/src/commands/id/identity.rs +++ b/crates/auths-cli/src/commands/id/identity.rs @@ -598,6 +598,8 @@ pub fn handle_id( let pkcs8_bytes = auths_core::crypto::signer::decrypt_keypair(&encrypted_key, &pass) .context("Failed to decrypt key")?; let keypair = auths_id::identity::helpers::load_keypair_from_der_or_seed(&pkcs8_bytes)?; + #[allow(clippy::disallowed_methods)] + // INVARIANT: hex::encode of Ed25519 pubkey always produces valid hex let public_key_hex = auths_verifier::PublicKeyHex::new_unchecked(hex::encode( keypair.public_key().as_ref(), )); diff --git a/crates/auths-cli/src/commands/id/register.rs b/crates/auths-cli/src/commands/id/register.rs index bdfdf789..4b3f1f06 100644 --- a/crates/auths-cli/src/commands/id/register.rs +++ b/crates/auths-cli/src/commands/id/register.rs @@ -65,7 +65,12 @@ pub fn handle_register(repo_path: &Path, registry: &str) -> Result<()> { Err(RegistrationError::NetworkError(e)) => { bail!("Failed to connect to registry server: {e}"); } - Err(RegistrationError::LocalDataError(e)) => { + Err( + e @ (RegistrationError::InvalidDidFormat { .. } + | RegistrationError::IdentityLoadError(_) + | RegistrationError::RegistryReadError(_) + | RegistrationError::SerializationError(_)), + ) => { bail!("{e}"); } Err(e) => { diff --git a/crates/auths-cli/src/commands/init/display.rs b/crates/auths-cli/src/commands/init/display.rs index 4fc56eee..3b038ff0 100644 --- a/crates/auths-cli/src/commands/init/display.rs +++ b/crates/auths-cli/src/commands/init/display.rs @@ -56,7 +56,12 @@ pub(crate) fn display_ci_result( pub(crate) fn display_agent_result(out: &Output, result: &auths_sdk::result::AgentIdentityResult) { out.print_heading("Agent Setup Complete!"); out.newline(); - out.println(&format!(" Identity: {}", out.info(&result.agent_did))); + let did_display = result + .agent_did + .as_ref() + .map(|d| d.to_string()) + .unwrap_or_else(|| "".to_string()); + out.println(&format!(" Identity: {}", out.info(&did_display))); let cap_display: Vec = result.capabilities.iter().map(|c| c.to_string()).collect(); out.println(&format!(" Capabilities: {}", cap_display.join(", "))); out.newline(); diff --git a/crates/auths-cli/src/commands/init/gather.rs b/crates/auths-cli/src/commands/init/gather.rs index 9d1a35fd..cb65e678 100644 --- a/crates/auths-cli/src/commands/init/gather.rs +++ b/crates/auths-cli/src/commands/init/gather.rs @@ -182,7 +182,12 @@ pub(crate) fn submit_registration( out.println(" Run `auths id register` when you're back online."); None } - Err(auths_sdk::error::RegistrationError::LocalDataError(e)) => { + Err( + e @ (auths_sdk::error::RegistrationError::InvalidDidFormat { .. } + | auths_sdk::error::RegistrationError::IdentityLoadError(_) + | auths_sdk::error::RegistrationError::RegistryReadError(_) + | auths_sdk::error::RegistrationError::SerializationError(_)), + ) => { out.print_warn(&format!("Could not prepare registration payload: {e}")); out.println(" Run `auths id register` to retry."); None diff --git a/crates/auths-cli/src/commands/org.rs b/crates/auths-cli/src/commands/org.rs index 26a17daa..fc616bc3 100644 --- a/crates/auths-cli/src/commands/org.rs +++ b/crates/auths-cli/src/commands/org.rs @@ -662,6 +662,8 @@ pub fn handle_org( let (stored_did, _role, _encrypted_key) = key_storage .load_key(&signer_alias) .with_context(|| format!("Failed to load signer key '{}'", signer_alias))?; + #[allow(clippy::disallowed_methods)] + // INVARIANT: hex::encode of resolved Ed25519 pubkey always produces valid hex let admin_pk_hex = PublicKeyHex::new_unchecked(hex::encode( resolver .resolve(stored_did.as_str()) @@ -776,6 +778,8 @@ pub fn handle_org( let (stored_did, _role, _encrypted_key) = key_storage .load_key(&signer_alias) .with_context(|| format!("Failed to load signer key '{}'", signer_alias))?; + #[allow(clippy::disallowed_methods)] + // INVARIANT: hex::encode of resolved Ed25519 pubkey always produces valid hex let admin_pk_hex = PublicKeyHex::new_unchecked(hex::encode( resolver .resolve(stored_did.as_str()) diff --git a/crates/auths-core/src/policy/device.rs b/crates/auths-core/src/policy/device.rs index 012b7611..7ec63983 100644 --- a/crates/auths-core/src/policy/device.rs +++ b/crates/auths-core/src/policy/device.rs @@ -78,13 +78,13 @@ impl Action { /// ```rust /// use auths_core::policy::{Decision, device::{Action, authorize_device}}; /// use auths_verifier::core::{Attestation, Capability, Ed25519PublicKey, Ed25519Signature}; -/// use auths_verifier::types::DeviceDID; +/// use auths_verifier::types::{CanonicalDid, DeviceDID}; /// use chrono::Utc; /// /// let attestation = Attestation { /// version: 1, /// rid: "test".into(), -/// issuer: "did:keri:ETest".into(), +/// issuer: CanonicalDid::new_unchecked("did:keri:ETest"), /// subject: DeviceDID::new_unchecked("did:key:z6Mk..."), /// device_public_key: Ed25519PublicKey::from_bytes([0u8; 32]), /// identity_signature: Ed25519Signature::empty(), diff --git a/crates/auths-core/src/policy/org.rs b/crates/auths-core/src/policy/org.rs index 9a9bd20f..f148da36 100644 --- a/crates/auths-core/src/policy/org.rs +++ b/crates/auths-core/src/policy/org.rs @@ -57,13 +57,13 @@ use super::device::Action; /// ```rust /// use auths_core::policy::{Decision, device::Action, org::authorize_org_action}; /// use auths_verifier::core::{Attestation, Capability, Ed25519PublicKey, Ed25519Signature, Role}; -/// use auths_verifier::types::DeviceDID; +/// use auths_verifier::types::{CanonicalDid, DeviceDID}; /// use chrono::Utc; /// /// let membership = Attestation { /// version: 1, /// rid: "member".into(), -/// issuer: "did:keri:EOrg123".into(), +/// issuer: CanonicalDid::new_unchecked("did:keri:EOrg123"), /// subject: DeviceDID::new_unchecked("did:key:z6MkAlice"), /// device_public_key: Ed25519PublicKey::from_bytes([0u8; 32]), /// identity_signature: Ed25519Signature::empty(), diff --git a/crates/auths-id/src/storage/indexed.rs b/crates/auths-id/src/storage/indexed.rs index 4dc6526a..df5db0f3 100644 --- a/crates/auths-id/src/storage/indexed.rs +++ b/crates/auths-id/src/storage/indexed.rs @@ -91,7 +91,7 @@ impl IndexedAttestationStorage { issuer_did: IdentityDID::new_unchecked(att.issuer.as_str()), device_did: att.subject.clone(), git_ref: git_ref.to_string(), - commit_oid: CommitOid::new_unchecked(commit_oid), + commit_oid: CommitOid::parse(commit_oid).ok(), revoked_at: att.revoked_at, expires_at: att.expires_at, updated_at: att.timestamp.unwrap_or(now), diff --git a/crates/auths-id/src/storage/registry/backend.rs b/crates/auths-id/src/storage/registry/backend.rs index d6beee50..8aaac122 100644 --- a/crates/auths-id/src/storage/registry/backend.rs +++ b/crates/auths-id/src/storage/registry/backend.rs @@ -631,14 +631,13 @@ pub trait RegistryBackend: Send + Sync { source_filename: entry.filename.clone(), }); } else { - // Invalid entry - include with minimal info for debugging members.push(MemberView { did: entry.did.clone(), status, role: None, capabilities: vec![], - issuer: IdentityDID::new_unchecked(String::new()), - rid: ResourceId::new(String::new()), + issuer: entry.org.clone(), + rid: ResourceId::new(""), revoked_at: None, expires_at: None, timestamp: None, diff --git a/crates/auths-index/src/index.rs b/crates/auths-index/src/index.rs index 7ec82d45..b454e290 100644 --- a/crates/auths-index/src/index.rs +++ b/crates/auths-index/src/index.rs @@ -20,8 +20,8 @@ pub struct IndexedAttestation { pub device_did: DeviceDID, /// Git ref path (e.g., refs/auths/devices/nodes/...) pub git_ref: String, - /// Git commit OID for loading full attestation - pub commit_oid: CommitOid, + /// Git commit OID for loading full attestation (None when OID is not yet known) + pub commit_oid: Option, /// When this attestation was revoked, if applicable pub revoked_at: Option>, /// Optional expiration timestamp @@ -107,7 +107,7 @@ impl AttestationIndex { stmt.bind((2, att.issuer_did.as_str()))?; stmt.bind((3, att.device_did.as_str()))?; stmt.bind((4, att.git_ref.as_str()))?; - stmt.bind((5, att.commit_oid.as_str()))?; + stmt.bind((5, att.commit_oid.as_ref().map(|c| c.as_str())))?; stmt.bind((6, revoked_at_str.as_deref()))?; stmt.bind((7, expires_at_str.as_deref()))?; stmt.bind((8, updated_at_str.as_str()))?; @@ -259,7 +259,7 @@ impl AttestationIndex { let issuer_did: String = stmt.read(1)?; let device_did: String = stmt.read(2)?; let git_ref: String = stmt.read(3)?; - let commit_oid: String = stmt.read(4)?; + let commit_oid: Option = stmt.read(4)?; let revoked_at_str: Option = stmt.read(5)?; let expires_at_str: Option = stmt.read(6)?; let updated_at_str: String = stmt.read(7)?; @@ -281,7 +281,9 @@ impl AttestationIndex { issuer_did: IdentityDID::new_unchecked(issuer_did), device_did: DeviceDID::new_unchecked(device_did), git_ref, - commit_oid: CommitOid::new_unchecked(commit_oid), + commit_oid: commit_oid + .filter(|s| !s.is_empty()) + .and_then(|s| CommitOid::parse(&s).ok()), revoked_at, expires_at, updated_at, @@ -462,7 +464,7 @@ mod tests { issuer_did: IdentityDID::new_unchecked("did:key:issuer123"), device_did: DeviceDID::new_unchecked(device), git_ref: format!("refs/auths/devices/nodes/{}/signatures", device), - commit_oid: CommitOid::new_unchecked("abc123"), + commit_oid: None, revoked_at, expires_at: Some(Utc::now() + Duration::days(30)), updated_at: Utc::now(), diff --git a/crates/auths-index/src/rebuild.rs b/crates/auths-index/src/rebuild.rs index 96802cdc..bc9a1a03 100644 --- a/crates/auths-index/src/rebuild.rs +++ b/crates/auths-index/src/rebuild.rs @@ -123,7 +123,7 @@ fn extract_attestation_from_ref( issuer_did: IdentityDID::new_unchecked(&issuer_did), device_did: DeviceDID::new_unchecked(&device_did), git_ref: ref_name.to_string(), - commit_oid: CommitOid::new_unchecked(commit.id().to_string()), + commit_oid: CommitOid::parse(&commit.id().to_string()).ok(), revoked_at, expires_at, updated_at, diff --git a/crates/auths-index/src/schema.rs b/crates/auths-index/src/schema.rs index 0cc3ed28..97a99670 100644 --- a/crates/auths-index/src/schema.rs +++ b/crates/auths-index/src/schema.rs @@ -6,7 +6,7 @@ CREATE TABLE IF NOT EXISTS attestations ( issuer_did TEXT NOT NULL, device_did TEXT NOT NULL, git_ref TEXT NOT NULL, - commit_oid TEXT NOT NULL, + commit_oid TEXT, revoked_at TEXT, expires_at TEXT, updated_at TEXT NOT NULL diff --git a/crates/auths-radicle/src/attestation.rs b/crates/auths-radicle/src/attestation.rs index fa86c31e..37134670 100644 --- a/crates/auths-radicle/src/attestation.rs +++ b/crates/auths-radicle/src/attestation.rs @@ -224,11 +224,16 @@ impl TryFrom for Attestation { type Error = AttestationConversionError; fn try_from(rad: RadAttestation) -> Result { + #[allow(clippy::disallowed_methods)] + // INVARIANT: rad.canonical_payload.did is a validated radicle Did + let issuer = CanonicalDid::new_unchecked(rad.canonical_payload.did.to_string()); + #[allow(clippy::disallowed_methods)] // INVARIANT: rad.device_did is a validated radicle Did + let subject = DeviceDID::new_unchecked(rad.device_did.to_string()); Ok(Attestation { version: 1, rid: ResourceId::new(rad.canonical_payload.rid.to_string()), - issuer: CanonicalDid::new_unchecked(rad.canonical_payload.did.to_string()), - subject: DeviceDID::new_unchecked(rad.device_did.to_string()), + issuer, + subject, device_public_key: Ed25519PublicKey::try_from_slice(rad.device_public_key.as_ref()) .map_err(|_e| { AttestationConversionError::InvalidPublicKeyLength( @@ -288,6 +293,7 @@ impl TryFrom<&Attestation> for RadAttestation { } #[cfg(test)] +#[allow(clippy::disallowed_methods)] mod tests { use super::*; diff --git a/crates/auths-radicle/src/storage.rs b/crates/auths-radicle/src/storage.rs index 870fbb0d..b7534344 100644 --- a/crates/auths-radicle/src/storage.rs +++ b/crates/auths-radicle/src/storage.rs @@ -184,6 +184,7 @@ impl AuthsStorage for GitRadicleStorage { } fn load_key_state(&self, identity_did: &Did) -> Result { + #[allow(clippy::disallowed_methods)] // INVARIANT: identity_did is a validated radicle Did let did = IdentityDID::new_unchecked(identity_did.to_string()); let repo = self.lock_repo(); let events = self.read_kel_events(&repo, &did)?; @@ -204,6 +205,7 @@ impl AuthsStorage for GitRadicleStorage { device_did: &Did, identity_did: &Did, ) -> Result { + #[allow(clippy::disallowed_methods)] // INVARIANT: device_did is a validated radicle Did let dev_did = DeviceDID::new_unchecked(device_did.to_string()); let nid = device_did_to_nid(device_did)?; let did_key_ref = self.layout.device_did_key_ref(&nid); @@ -261,6 +263,8 @@ impl AuthsStorage for GitRadicleStorage { device_did: &Did, _repo_id: &RepoId, ) -> Result, BridgeError> { + #[allow(clippy::disallowed_methods)] + // INVARIANT: synthetic DID used only as error context identifier let did_context = IdentityDID::new_unchecked(format!("lookup:{device_did}")); let nid = device_did_to_nid(device_did)?; let sig_ref = self.layout.device_signatures_ref(&nid); diff --git a/crates/auths-radicle/src/verify.rs b/crates/auths-radicle/src/verify.rs index c406486c..966612a1 100644 --- a/crates/auths-radicle/src/verify.rs +++ b/crates/auths-radicle/src/verify.rs @@ -268,8 +268,11 @@ impl RadicleAuthsBridge for DefaultBridge { // Step 6: Evaluate policy (revocation, expiry) let decision = evaluate_compiled(&attestation, &self.policy, request.now).map_err(|e| { + #[allow(clippy::disallowed_methods)] + // INVARIANT: identity_did is a validated radicle Did + let did = IdentityDID::new_unchecked(identity_did.to_string()); BridgeError::PolicyEvaluation { - did: IdentityDID::new_unchecked(identity_did.to_string()), + did, reason: e.to_string(), } })?; diff --git a/crates/auths-sdk/src/device.rs b/crates/auths-sdk/src/device.rs index 88e23489..8ad848c4 100644 --- a/crates/auths-sdk/src/device.rs +++ b/crates/auths-sdk/src/device.rs @@ -141,7 +141,7 @@ pub fn revoke_device( ctx.passphrase_provider.as_ref(), identity_key_alias, ) - .map_err(|e| DeviceError::AttestationError(format!("revocation signing failed: {e}")))?; + .map_err(DeviceError::AttestationError)?; ctx.attestation_sink .export(&auths_verifier::VerifiedAttestation::dangerous_from_unchecked(revocation)) @@ -222,7 +222,7 @@ pub fn extend_device( None, None, ) - .map_err(|e| DeviceExtensionError::AttestationFailed(e.to_string()))?; + .map_err(DeviceExtensionError::AttestationFailed)?; ctx.attestation_sink .export(&auths_verifier::VerifiedAttestation::dangerous_from_unchecked(extended.clone())) @@ -280,10 +280,10 @@ fn extract_device_key( if let Some(ref expected) = config.device_did && expected != &device_did.to_string() { - return Err(DeviceError::AttestationError(format!( - "--device-did {} does not match key-derived DID {}", - expected, device_did - ))); + return Err(DeviceError::DeviceDidMismatch { + expected: expected.clone(), + actual: device_did.to_string(), + }); } Ok((device_did, pk_bytes)) @@ -313,7 +313,7 @@ fn sign_and_persist_attestation( None, None, ) - .map_err(|e| DeviceError::AttestationError(format!("attestation creation failed: {e}")))?; + .map_err(DeviceError::AttestationError)?; let attestation_rid = attestation.rid.to_string(); diff --git a/crates/auths-sdk/src/error.rs b/crates/auths-sdk/src/error.rs index cea1cb85..039fc6e0 100644 --- a/crates/auths-sdk/src/error.rs +++ b/crates/auths-sdk/src/error.rs @@ -72,7 +72,11 @@ pub enum SetupError { /// Setting a git configuration key failed. #[error("git config error: {0}")] - GitConfigError(String), + GitConfigError(#[source] crate::ports::git_config::GitConfigError), + + /// Setup configuration parameters are invalid. + #[error("invalid setup config: {0}")] + InvalidSetupConfig(String), /// Remote registry registration failed. #[error("registration failed: {0}")] @@ -112,7 +116,16 @@ pub enum DeviceError { /// Attestation creation or validation failed. #[error("attestation error: {0}")] - AttestationError(String), + AttestationError(#[source] auths_verifier::error::AttestationError), + + /// The device DID derived from the key does not match the expected DID. + #[error("device DID mismatch: expected {expected}, got {actual}")] + DeviceDidMismatch { + /// The expected device DID. + expected: String, + /// The actual device DID derived from the key. + actual: String, + }, /// A cryptographic operation failed. #[error("crypto error: {0}")] @@ -156,7 +169,7 @@ pub enum DeviceExtensionError { /// Creating a new attestation failed. #[error("attestation creation failed: {0}")] - AttestationFailed(String), + AttestationFailed(#[source] auths_verifier::error::AttestationError), /// A storage operation failed. #[error("storage error: {0}")] @@ -233,9 +246,24 @@ pub enum RegistrationError { #[error("network error: {0}")] NetworkError(#[source] auths_core::ports::network::NetworkError), - /// Local identity or attestation data is invalid. - #[error("local data error: {0}")] - LocalDataError(String), + /// The local DID format is invalid. + #[error("invalid DID format: {did}")] + InvalidDidFormat { + /// The DID that failed validation. + did: String, + }, + + /// Loading the local identity failed. + #[error("identity load error: {0}")] + IdentityLoadError(#[source] auths_id::error::StorageError), + + /// Reading from the local registry failed. + #[error("registry read error: {0}")] + RegistryReadError(#[source] auths_id::storage::registry::backend::RegistryError), + + /// Serialization of identity data failed. + #[error("serialization error: {0}")] + SerializationError(#[source] serde_json::Error), } impl From for SetupError { @@ -270,6 +298,7 @@ impl AuthsErrorInfo for SetupError { Self::CryptoError(e) => e.error_code(), Self::StorageError(_) => "AUTHS-E5003", Self::GitConfigError(_) => "AUTHS-E5004", + Self::InvalidSetupConfig(_) => "AUTHS-E5007", Self::RegistrationFailed(_) => "AUTHS-E5005", Self::PlatformVerificationFailed(_) => "AUTHS-E5006", } @@ -288,6 +317,7 @@ impl AuthsErrorInfo for SetupError { Self::GitConfigError(_) => { Some("Ensure Git is configured: git config --global user.name/email") } + Self::InvalidSetupConfig(_) => Some("Check identity setup configuration parameters"), Self::RegistrationFailed(_) => Some("Check network connectivity and try again"), Self::PlatformVerificationFailed(_) => None, } @@ -300,6 +330,7 @@ impl AuthsErrorInfo for DeviceError { Self::IdentityNotFound { .. } => "AUTHS-E5101", Self::DeviceNotFound { .. } => "AUTHS-E5102", Self::AttestationError(_) => "AUTHS-E5103", + Self::DeviceDidMismatch { .. } => "AUTHS-E5105", Self::CryptoError(e) => e.error_code(), Self::StorageError(_) => "AUTHS-E5104", } @@ -310,6 +341,7 @@ impl AuthsErrorInfo for DeviceError { Self::IdentityNotFound { .. } => Some("Run `auths init` to create an identity first"), Self::DeviceNotFound { .. } => Some("Run `auths device list` to see linked devices"), Self::AttestationError(_) => None, + Self::DeviceDidMismatch { .. } => Some("Check that --device-did matches the key alias"), Self::CryptoError(e) => e.suggestion(), Self::StorageError(_) => Some("Check file permissions and disk space"), } @@ -372,7 +404,10 @@ impl AuthsErrorInfo for RegistrationError { Self::AlreadyRegistered => "AUTHS-E5401", Self::QuotaExceeded => "AUTHS-E5402", Self::NetworkError(e) => e.error_code(), - Self::LocalDataError(_) => "AUTHS-E5403", + Self::InvalidDidFormat { .. } => "AUTHS-E5403", + Self::IdentityLoadError(_) => "AUTHS-E5404", + Self::RegistryReadError(_) => "AUTHS-E5405", + Self::SerializationError(_) => "AUTHS-E5406", } } @@ -381,7 +416,12 @@ impl AuthsErrorInfo for RegistrationError { Self::AlreadyRegistered => None, Self::QuotaExceeded => Some("Wait a few minutes and try again"), Self::NetworkError(e) => e.suggestion(), - Self::LocalDataError(_) => Some("Run `auths doctor` to check local identity data"), + Self::InvalidDidFormat { .. } => { + Some("Run `auths doctor` to check local identity data") + } + Self::IdentityLoadError(_) => Some("Run `auths doctor` to check local identity data"), + Self::RegistryReadError(_) => Some("Run `auths doctor` to check local identity data"), + Self::SerializationError(_) => Some("Run `auths doctor` to check local identity data"), } } } @@ -574,11 +614,9 @@ pub enum OrgError { #[error("key storage error: {0}")] KeyStorage(String), - // TECH-DEBT(fn-33): migrate Storage(String) to typed SdkStorageError variant - // (call sites in workflows/org.rs, not in the fn-33.1 scope) /// A storage operation failed. #[error("storage error: {0}")] - Storage(String), + Storage(#[source] auths_id::storage::registry::backend::RegistryError), } /// Re-export from `auths-core` — defined there to avoid a circular dependency with @@ -618,8 +656,7 @@ pub enum ApprovalError { #[error("approval partially applied — attestation stored but nonce/cleanup failed: {0}")] PartialApproval(String), - // TECH-DEBT(fn-33): migrate ApprovalStorage(String) to typed SdkStorageError variant /// A storage operation failed. #[error("storage error: {0}")] - ApprovalStorage(String), + ApprovalStorage(#[source] SdkStorageError), } diff --git a/crates/auths-sdk/src/pairing/mod.rs b/crates/auths-sdk/src/pairing/mod.rs index 428a4a49..04fb85ff 100644 --- a/crates/auths-sdk/src/pairing/mod.rs +++ b/crates/auths-sdk/src/pairing/mod.rs @@ -275,7 +275,10 @@ pub fn verify_device_did(device_pubkey: &[u8; 32], claimed_did: &str) -> Result< use auths_verifier::types::DeviceDID; let derived = DeviceDID::from_ed25519(device_pubkey); - let claimed = DeviceDID::new_unchecked(claimed_did.to_string()); + let claimed = DeviceDID::parse(claimed_did).map_err(|_| PairingError::DidMismatch { + response: claimed_did.to_string(), + derived: derived.to_string(), + })?; if derived != claimed { return Err(PairingError::DidMismatch { @@ -343,7 +346,8 @@ pub fn create_pairing_attestation( }) .collect::, _>>()?; - let target_did = DeviceDID::new_unchecked(params.device_did_str.to_string()); + let target_did = DeviceDID::parse(params.device_did_str) + .map_err(|e| PairingError::AttestationFailed(format!("invalid device DID: {e}")))?; let secure_signer = StorageSigner::new(Arc::clone(¶ms.key_storage)); let attestation = create_signed_attestation( @@ -844,7 +848,9 @@ pub async fn join_pairing_session( .map_err(|e| PairingError::StorageError(e.to_string()))?; Ok(PairingCompletionResult::Success { - device_did: DeviceDID::new_unchecked(pairing_response.device_did), + device_did: DeviceDID::parse(&pairing_response.device_did).map_err(|e| { + PairingError::AttestationFailed(format!("invalid device DID in response: {e}")) + })?, device_name: None, }) } diff --git a/crates/auths-sdk/src/registration.rs b/crates/auths-sdk/src/registration.rs index 3b5f8ce9..a4afeaef 100644 --- a/crates/auths-sdk/src/registration.rs +++ b/crates/auths-sdk/src/registration.rs @@ -54,27 +54,22 @@ pub async fn register_identity( ) -> Result { let identity = identity_storage .load_identity() - .map_err(|e| RegistrationError::LocalDataError(e.to_string()))?; + .map_err(RegistrationError::IdentityLoadError)?; let did_prefix = identity .controller_did .as_str() .strip_prefix("did:keri:") - .ok_or_else(|| { - RegistrationError::LocalDataError(format!( - "Invalid DID format, expected 'did:keri:': {}", - identity.controller_did - )) + .ok_or_else(|| RegistrationError::InvalidDidFormat { + did: identity.controller_did.to_string(), })?; let prefix = Prefix::new_unchecked(did_prefix.to_string()); - let inception = registry.get_event(&prefix, 0).map_err(|_| { - RegistrationError::LocalDataError( - "No KEL events found for identity. The identity may be corrupted.".to_string(), - ) - })?; - let inception_event = serde_json::to_value(&inception) - .map_err(|e| RegistrationError::LocalDataError(e.to_string()))?; + let inception = registry + .get_event(&prefix, 0) + .map_err(RegistrationError::RegistryReadError)?; + let inception_event = + serde_json::to_value(&inception).map_err(RegistrationError::SerializationError)?; let attestations = attestation_source .load_all_attestations() @@ -90,8 +85,7 @@ pub async fn register_identity( proof_url, }; - let json_body = serde_json::to_vec(&payload) - .map_err(|e| RegistrationError::LocalDataError(e.to_string()))?; + let json_body = serde_json::to_vec(&payload).map_err(RegistrationError::SerializationError)?; let registry_url = registry_url.trim_end_matches('/'); let response = registry_client diff --git a/crates/auths-sdk/src/result.rs b/crates/auths-sdk/src/result.rs index f378ab76..88cc89c5 100644 --- a/crates/auths-sdk/src/result.rs +++ b/crates/auths-sdk/src/result.rs @@ -53,15 +53,15 @@ pub struct CiIdentityResult { /// ```ignore /// let result = initialize(IdentityConfig::agent(alias, path), &ctx, keychain, &signer, &provider, None)?; /// if let InitializeResult::Agent(r) = result { -/// println!("Agent {} delegated by {}", r.agent_did, r.parent_did); +/// println!("Agent {:?} delegated by {:?}", r.agent_did, r.parent_did); /// } /// ``` #[derive(Debug, Clone)] pub struct AgentIdentityResult { - /// The DID of the newly created agent identity. - pub agent_did: IdentityDID, - /// The DID of the parent identity that delegated authority. - pub parent_did: IdentityDID, + /// The DID of the newly created agent identity (None for dry-run proposals). + pub agent_did: Option, + /// The DID of the parent identity that delegated authority (None if no parent). + pub parent_did: Option, /// The capabilities granted to the agent. pub capabilities: Vec, } diff --git a/crates/auths-sdk/src/setup.rs b/crates/auths-sdk/src/setup.rs index b79c32f7..3edeb561 100644 --- a/crates/auths-sdk/src/setup.rs +++ b/crates/auths-sdk/src/setup.rs @@ -169,8 +169,10 @@ fn initialize_agent( .map_err(|e| SetupError::StorageError(e.into()))?; return Ok(AgentIdentityResult { - agent_did: bundle.agent_did, - parent_did: IdentityDID::new_unchecked(config.parent_identity_did.unwrap_or_default()), + agent_did: Some(bundle.agent_did), + parent_did: config + .parent_identity_did + .and_then(|s| IdentityDID::parse(&s).ok()), capabilities: config.capabilities, }); } @@ -344,10 +346,10 @@ fn configure_git_signing( return Ok(false); } let git_config = git_config.ok_or_else(|| { - SetupError::GitConfigError("GitConfigProvider required for non-Skip scope".into()) + SetupError::InvalidSetupConfig("GitConfigProvider required for non-Skip scope".into()) })?; let sign_binary_path = sign_binary_path.ok_or_else(|| { - SetupError::GitConfigError("sign_binary_path required for non-Skip scope".into()) + SetupError::InvalidSetupConfig("sign_binary_path required for non-Skip scope".into()) })?; set_git_signing_config(key_alias, git_config, sign_binary_path)?; Ok(true) @@ -358,9 +360,9 @@ fn set_git_signing_config( git_config: &dyn GitConfigProvider, sign_binary_path: &Path, ) -> Result<(), SetupError> { - let auths_sign_str = sign_binary_path - .to_str() - .ok_or_else(|| SetupError::GitConfigError("auths-sign path is not valid UTF-8".into()))?; + let auths_sign_str = sign_binary_path.to_str().ok_or_else(|| { + SetupError::InvalidSetupConfig("auths-sign path is not valid UTF-8".into()) + })?; let signing_key = format!("auths:{}", key_alias); let configs: &[(&str, &str)] = &[ ("gpg.format", "ssh"), @@ -372,7 +374,7 @@ fn set_git_signing_config( for (key, val) in configs { git_config .set(key, val) - .map_err(|e| SetupError::GitConfigError(e.to_string()))?; + .map_err(SetupError::GitConfigError)?; } Ok(()) } @@ -467,10 +469,11 @@ fn build_agent_identity_proposal( config: &CreateAgentIdentityConfig, ) -> Result { Ok(AgentIdentityResult { - agent_did: IdentityDID::new_unchecked(format!("did:keri:E", config.alias)), - parent_did: IdentityDID::new_unchecked( - config.parent_identity_did.clone().unwrap_or_default(), - ), + agent_did: None, + parent_did: config + .parent_identity_did + .as_deref() + .and_then(|s| IdentityDID::parse(s).ok()), capabilities: config.capabilities.clone(), }) } diff --git a/crates/auths-sdk/src/workflows/allowed_signers.rs b/crates/auths-sdk/src/workflows/allowed_signers.rs index e643038e..499c8567 100644 --- a/crates/auths-sdk/src/workflows/allowed_signers.rs +++ b/crates/auths-sdk/src/workflows/allowed_signers.rs @@ -530,7 +530,11 @@ fn parse_entry_line( }; let public_key = Ed25519PublicKey::from_bytes(raw_bytes); - let principal = parse_principal(principal_str); + let principal = + parse_principal(principal_str).ok_or_else(|| AllowedSignersError::ParseError { + line: line_num, + detail: format!("unrecognized principal format: {}", principal_str), + })?; Ok(SignerEntry { principal, @@ -539,18 +543,20 @@ fn parse_entry_line( }) } -fn parse_principal(s: &str) -> SignerPrincipal { +fn parse_principal(s: &str) -> Option { if let Some(local) = s.strip_suffix("@auths.local") { let did_str = format!("did:key:{}", local); - return SignerPrincipal::DeviceDid(DeviceDID::new_unchecked(did_str)); + if let Ok(did) = DeviceDID::parse(&did_str) { + return Some(SignerPrincipal::DeviceDid(did)); + } } - if s.starts_with("did:key:") { - return SignerPrincipal::DeviceDid(DeviceDID::new_unchecked(s)); + if let Ok(did) = DeviceDID::parse(s) { + return Some(SignerPrincipal::DeviceDid(did)); } - match EmailAddress::new(s) { - Ok(addr) => SignerPrincipal::Email(addr), - Err(_) => SignerPrincipal::DeviceDid(DeviceDID::new_unchecked(s)), + if let Ok(addr) = EmailAddress::new(s) { + return Some(SignerPrincipal::Email(addr)); } + None } fn format_entry(entry: &SignerEntry) -> String { @@ -604,12 +610,12 @@ mod tests { #[test] fn principal_roundtrip() { let email_p = SignerPrincipal::Email(EmailAddress::new("user@example.com").unwrap()); - let parsed = parse_principal(&email_p.to_string()); + let parsed = parse_principal(&email_p.to_string()).unwrap(); assert_eq!(parsed, email_p); let did = DeviceDID::new_unchecked("did:key:z6MkTest123"); let did_p = SignerPrincipal::DeviceDid(did); - let parsed = parse_principal(&did_p.to_string()); + let parsed = parse_principal(&did_p.to_string()).unwrap(); assert_eq!(parsed, did_p); } diff --git a/crates/auths-sdk/src/workflows/org.rs b/crates/auths-sdk/src/workflows/org.rs index c209e11c..d48fbc20 100644 --- a/crates/auths-sdk/src/workflows/org.rs +++ b/crates/auths-sdk/src/workflows/org.rs @@ -110,7 +110,7 @@ pub(crate) fn find_admin( } ControlFlow::Continue(()) }) - .map_err(|e| OrgError::Storage(e.to_string()))?; + .map_err(OrgError::Storage)?; found.ok_or_else(|| OrgError::AdminNotFound { org: org_prefix.to_owned(), @@ -145,7 +145,7 @@ pub(crate) fn find_member( } ControlFlow::Continue(()) }) - .map_err(|e| OrgError::Storage(e.to_string()))?; + .map_err(OrgError::Storage)?; Ok(found) } @@ -369,7 +369,7 @@ pub fn add_organization_member( ctx.registry .store_org_member(&cmd.org_prefix, &attestation) - .map_err(|e| OrgError::Storage(e.to_string()))?; + .map_err(OrgError::Storage)?; Ok(attestation) } @@ -429,7 +429,7 @@ pub fn revoke_organization_member( ctx.registry .store_org_member(&cmd.org_prefix, &revocation) - .map_err(|e| OrgError::Storage(e.to_string()))?; + .map_err(OrgError::Storage)?; Ok(revocation) } @@ -475,7 +475,7 @@ pub fn update_member_capabilities( backend .store_org_member(&cmd.org_prefix, &updated) - .map_err(|e| OrgError::Storage(e.to_string()))?; + .map_err(OrgError::Storage)?; Ok(updated) } @@ -516,7 +516,7 @@ pub fn update_organization_member( backend .store_org_member(&cmd.org_prefix, &updated) - .map_err(|e| OrgError::Storage(e.to_string()))?; + .map_err(OrgError::Storage)?; Ok(updated) } diff --git a/crates/auths-storage/src/git/adapter.rs b/crates/auths-storage/src/git/adapter.rs index a1774180..d6cd14c1 100644 --- a/crates/auths-storage/src/git/adapter.rs +++ b/crates/auths-storage/src/git/adapter.rs @@ -1207,12 +1207,15 @@ impl RegistryBackend for GitRegistryBackend { // Index update: best-effort, Git is source of truth #[cfg(feature = "indexed-storage")] if let Some(index) = &self.index { + #[allow(clippy::disallowed_methods)] + // INVARIANT: attestation.issuer is a validated CanonicalDid + let issuer_did = IdentityDID::new_unchecked(attestation.issuer.as_str()); let indexed = auths_index::IndexedAttestation { rid: attestation.rid.clone(), - issuer_did: IdentityDID::new_unchecked(attestation.issuer.as_str()), + issuer_did, device_did: attestation.subject.clone(), git_ref: REGISTRY_REF.to_string(), - commit_oid: auths_verifier::CommitOid::new_unchecked(""), + commit_oid: None, revoked_at: attestation.revoked_at, expires_at: attestation.expires_at, updated_at: attestation.timestamp.unwrap_or_else(|| self.clock.now()), @@ -1328,9 +1331,15 @@ impl RegistryBackend for GitRegistryBackend { // Level 3: device DIDs if let Err(e) = navigator.visit_dir(&s2_parts, |sanitized_did| { - // Convert back to proper DID format - let did = unsanitize_did(sanitized_did); - visitor(&DeviceDID::new_unchecked(did)) + let did_str = unsanitize_did(sanitized_did); + let did = match DeviceDID::parse(&did_str) { + Ok(d) => d, + Err(_) => { + log::warn!("Skipping unparseable DID from tree: {}", did_str); + return ControlFlow::Continue(()); + } + }; + visitor(&did) }) { captured_error.set(Some(e)); return ControlFlow::Break(()); @@ -1382,10 +1391,16 @@ impl RegistryBackend for GitRegistryBackend { // Index update: best-effort, Git is source of truth #[cfg(feature = "indexed-storage")] if let Some(index) = &self.index { + #[allow(clippy::disallowed_methods)] + // INVARIANT: org is a validated KERI prefix from registry storage + let org_prefix = auths_verifier::keri::Prefix::new_unchecked(org.to_string()); + #[allow(clippy::disallowed_methods)] + // INVARIANT: member.issuer is a validated CanonicalDid + let issuer_did = IdentityDID::new_unchecked(member.issuer.as_str()); let indexed = auths_index::IndexedOrgMember { - org_prefix: auths_verifier::keri::Prefix::new_unchecked(org.to_string()), + org_prefix, member_did: member.subject.clone(), - issuer_did: IdentityDID::new_unchecked(member.issuer.as_str()), + issuer_did, rid: member.rid.clone(), revoked_at: member.revoked_at, expires_at: member.expires_at, @@ -1432,9 +1447,14 @@ impl RegistryBackend for GitRegistryBackend { return ControlFlow::Continue(()); }; - // Derive DID from filename let did_str = unsanitize_did(sanitized_did); - let did = DeviceDID::new_unchecked(did_str.clone()); + let did = match DeviceDID::parse(&did_str) { + Ok(d) => d, + Err(_) => { + log::warn!("Skipping unparseable member DID: {}", did_str); + return ControlFlow::Continue(()); + } + }; // Read blob and parse attestation let full_path = paths::child(&members_path, filename); @@ -1451,11 +1471,15 @@ impl RegistryBackend for GitRegistryBackend { }) // Validate issuer matches expected org issuer (hard invariant) } else if att.issuer.as_str() != expected_issuer { + #[allow(clippy::disallowed_methods)] + // INVARIANT: expected_issuer is derived from validated org KERI prefix via expected_org_issuer() + let expected = IdentityDID::new_unchecked(expected_issuer.clone()); + #[allow(clippy::disallowed_methods)] + // INVARIANT: att.issuer is a validated CanonicalDid from deserialized attestation + let actual = IdentityDID::new_unchecked(att.issuer.as_str()); Err(MemberInvalidReason::IssuerMismatch { - expected_issuer: IdentityDID::new_unchecked( - expected_issuer.clone(), - ), - actual_issuer: IdentityDID::new_unchecked(att.issuer.as_str()), + expected_issuer: expected, + actual_issuer: actual, }) } else { Ok(att) @@ -1467,8 +1491,11 @@ impl RegistryBackend for GitRegistryBackend { Err(e) => Err(MemberInvalidReason::Other(e.to_string())), }; + #[allow(clippy::disallowed_methods)] + // INVARIANT: org is a validated KERI prefix from registry storage + let org_did = IdentityDID::new_unchecked(format!("did:keri:{}", org)); let entry = OrgMemberEntry { - org: IdentityDID::new_unchecked(format!("did:keri:{}", org)), + org: org_did, did, filename: filename.to_string(), attestation, @@ -1569,17 +1596,23 @@ impl RegistryBackend for GitRegistryBackend { let members: Vec = indexed .into_iter() - .map(|m| MemberView { - did: m.member_did.clone(), - status: MemberStatus::Active, - role: None, - capabilities: vec![], - issuer: auths_core::storage::keychain::IdentityDID::new_unchecked(m.issuer_did), - rid: m.rid, - revoked_at: m.revoked_at, - expires_at: m.expires_at, - timestamp: None, - source_filename: String::new(), + .map(|m| { + #[allow(clippy::disallowed_methods)] + // INVARIANT: m.issuer_did comes from indexed storage, originally a validated CanonicalDid + let issuer = + auths_core::storage::keychain::IdentityDID::new_unchecked(m.issuer_did); + MemberView { + did: m.member_did.clone(), + status: MemberStatus::Active, + role: None, + capabilities: vec![], + issuer, + rid: m.rid, + revoked_at: m.revoked_at, + expires_at: m.expires_at, + timestamp: None, + source_filename: String::new(), + } }) .collect(); @@ -1789,10 +1822,16 @@ pub fn rebuild_org_members_from_registry( stats.refs_scanned += 1; if let Ok(att) = &entry.attestation { + #[allow(clippy::disallowed_methods)] + // INVARIANT: org_prefix is a validated KERI prefix from visit_orgs + let prefix = auths_verifier::keri::Prefix::new_unchecked(org_prefix.clone()); + #[allow(clippy::disallowed_methods)] + // INVARIANT: att.issuer is a validated CanonicalDid from deserialized attestation + let issuer_did = IdentityDID::new_unchecked(att.issuer.as_str()); let indexed = IndexedOrgMember { - org_prefix: auths_verifier::keri::Prefix::new_unchecked(org_prefix.clone()), + org_prefix: prefix, member_did: entry.did.clone(), - issuer_did: IdentityDID::new_unchecked(att.issuer.as_str()), + issuer_did, rid: att.rid.clone(), revoked_at: att.revoked_at, expires_at: att.expires_at, @@ -4115,6 +4154,7 @@ mod index_consistency_tests { /// `expected_org_issuer` validation in `visit_org_member_attestations`). /// /// `org_prefix` must be a raw KERI prefix (e.g. "EOrg1234567890"), NOT a full DID. + #[allow(clippy::disallowed_methods)] fn make_org_attestation(org_prefix: &str, did_suffix: &str, rid: &str) -> Attestation { Attestation { version: 1, diff --git a/crates/auths-storage/src/git/attestation_adapter.rs b/crates/auths-storage/src/git/attestation_adapter.rs index ce9f800e..1fe67498 100644 --- a/crates/auths-storage/src/git/attestation_adapter.rs +++ b/crates/auths-storage/src/git/attestation_adapter.rs @@ -218,7 +218,7 @@ impl AttestationSink for RegistryAttestationStorage { issuer_did: IdentityDID::new_unchecked(attestation.issuer.as_str()), device_did: attestation.subject.clone(), git_ref, - commit_oid: auths_verifier::CommitOid::new_unchecked(""), + commit_oid: None, revoked_at: attestation.revoked_at, expires_at: attestation.expires_at, updated_at: attestation.timestamp.unwrap_or_else(chrono::Utc::now), diff --git a/crates/auths-storage/src/git/identity_adapter.rs b/crates/auths-storage/src/git/identity_adapter.rs index 2013ff4b..72c07778 100644 --- a/crates/auths-storage/src/git/identity_adapter.rs +++ b/crates/auths-storage/src/git/identity_adapter.rs @@ -345,8 +345,11 @@ impl IdentityStorage for RegistryIdentityStorage { // Load metadata let metadata = self.load_metadata(&prefix)?; + #[allow(clippy::disallowed_methods)] + // INVARIANT: controller_did is derived from a validated KERI prefix via format!("did:keri:{}", prefix_str) + let controller_did = IdentityDID::new_unchecked(controller_did); Ok(ManagedIdentity { - controller_did: IdentityDID::new_unchecked(controller_did), + controller_did, storage_id: self.get_storage_id(), metadata, }) diff --git a/crates/auths-storage/src/git/standalone_attestation.rs b/crates/auths-storage/src/git/standalone_attestation.rs index 9aa0f289..51b7a334 100644 --- a/crates/auths-storage/src/git/standalone_attestation.rs +++ b/crates/auths-storage/src/git/standalone_attestation.rs @@ -4,6 +4,7 @@ use auths_id::storage::layout::{ StorageLayoutConfig, attestation_blob_name, attestation_ref_for_device, default_attestation_prefixes, }; +use auths_id::storage::registry::shard::unsanitize_did; use auths_verifier::core::Attestation; use auths_verifier::types::DeviceDID; use git2::{ErrorCode, Repository, Tree}; @@ -198,7 +199,12 @@ impl AttestationSource for GitAttestationStorage { sanitized_did, full_ref_name ); - discovered_dids.insert(DeviceDID::new_unchecked(sanitized_did)); + let did_str = unsanitize_did(sanitized_did); + if let Ok(did) = DeviceDID::parse(&did_str) { + discovered_dids.insert(did); + } else { + log::warn!("Skipping unparseable DID from ref: {}", did_str); + } } } } diff --git a/crates/auths-storage/src/git/standalone_identity.rs b/crates/auths-storage/src/git/standalone_identity.rs index 755c027c..445f8324 100644 --- a/crates/auths-storage/src/git/standalone_identity.rs +++ b/crates/auths-storage/src/git/standalone_identity.rs @@ -64,9 +64,12 @@ impl IdentityStorage for GitIdentityStorage { let identity_ref_name = identity_ref(&self.config); let identity_blob_filename = identity_blob_name(&self.config); + #[allow(clippy::disallowed_methods)] + // INVARIANT: controller_did is a validated DID string from the caller (SDK layer) + let controller_did = IdentityDID::new_unchecked(controller_did); let stored_data = StoredIdentityData { version: 1, - controller_did: IdentityDID::new_unchecked(controller_did), + controller_did, metadata, }; let json_bytes = serde_json::to_vec_pretty(&stored_data)?; diff --git a/crates/auths-verifier/src/core.rs b/crates/auths-verifier/src/core.rs index b6bf5d9b..a71b57dd 100644 --- a/crates/auths-verifier/src/core.rs +++ b/crates/auths-verifier/src/core.rs @@ -1331,6 +1331,7 @@ impl PartialEq<&str> for PolicyId { } #[cfg(test)] +#[allow(clippy::disallowed_methods)] mod tests { use super::*; diff --git a/crates/auths-verifier/src/verify.rs b/crates/auths-verifier/src/verify.rs index 8680a36f..530326c7 100644 --- a/crates/auths-verifier/src/verify.rs +++ b/crates/auths-verifier/src/verify.rs @@ -559,7 +559,7 @@ async fn verify_single_attestation( } #[cfg(all(test, not(target_arch = "wasm32")))] -#[allow(clippy::unwrap_used, clippy::expect_used)] +#[allow(clippy::unwrap_used, clippy::expect_used, clippy::disallowed_methods)] mod tests { use super::*; use crate::clock::ClockProvider; diff --git a/docs/plans/launch_cleaning.md b/docs/plans/launch_cleaning.md index 3f4baa2c..f9f75584 100644 --- a/docs/plans/launch_cleaning.md +++ b/docs/plans/launch_cleaning.md @@ -120,13 +120,13 @@ Fuzz targets exist for `attestation_parse`, `did_parse`, and `verify_chain` in ` **Issues requiring attention before launch:** -**P0 — `verify-options` pass-through in `auths-sign`:** +~~**P0 — `verify-options` pass-through in `auths-sign`:**~~ In `bin/sign.rs`, `args.verify_options` (a `Vec` populated from CLI `--verify-option` flags) is passed directly as arguments to `ssh-keygen` via `.arg("-O").arg(opt)` (lines ~198–199 and ~230–231). While `Command::new` with explicit `.arg()` calls is not shell injection, a crafted `-O` value like `no-touch-required` or a future `ssh-keygen` flag could alter verification semantics. These options should be validated against an allowlist of known-safe `verify-time=` patterns before being passed through. This binary is callable from CI environments with attacker-influenced inputs. -**P1 — `DeviceDID` and `IdentityDID` inner values are publicly accessible:** +~~**P1 — `DeviceDID` and `IdentityDID` inner values are publicly accessible:**~~ `DeviceDID(pub String)` and `IdentityDID(pub String)` can be constructed with arbitrary strings without parsing. The DID format (`did:keri:...`) is not validated at construction. A malformed DID that bypasses newtypes can reach storage and the KEL resolver. -**P2 — `commands/emergency.rs:8341` writes `frozen_at: chrono::Utc::now()` into a freeze record:** +~~**P2 — `commands/emergency.rs:8341` writes `frozen_at: chrono::Utc::now()` into a freeze record:**~~ This timestamp is written to the git ref store and is later used to compute `expires_description()`. Since the clock is not injected, replay or time-skew attacks on freeze state cannot be tested. This is lower severity but relevant for enterprise audit trail integrity. --- diff --git a/packages/auths-node/src/attestation_query.rs b/packages/auths-node/src/attestation_query.rs index d739a4b2..77f093b5 100644 --- a/packages/auths-node/src/attestation_query.rs +++ b/packages/auths-node/src/attestation_query.rs @@ -99,6 +99,6 @@ pub fn get_latest_attestation( ) })?; let group = AttestationGroup::from_list(all); - let did = DeviceDID::new_unchecked(device_did); + let did = DeviceDID::parse(&device_did).map_err(|e| format_error("AUTHS_INVALID_INPUT", e))?; Ok(group.latest(&did).map(attestation_to_napi)) } diff --git a/packages/auths-node/src/device.rs b/packages/auths-node/src/device.rs index af58bab1..ac089e54 100644 --- a/packages/auths-node/src/device.rs +++ b/packages/auths-node/src/device.rs @@ -178,7 +178,8 @@ pub fn extend_device_authorization( let ext_config = DeviceExtensionConfig { repo_path: repo, - device_did: DeviceDID::new_unchecked(&device_did), + device_did: DeviceDID::parse(&device_did) + .map_err(|e| format_error("AUTHS_INVALID_INPUT", e))?, days, identity_key_alias: alias, device_key_alias: None, diff --git a/packages/auths-node/src/helpers.rs b/packages/auths-node/src/helpers.rs index daae67d0..61c9b039 100644 --- a/packages/auths-node/src/helpers.rs +++ b/packages/auths-node/src/helpers.rs @@ -42,7 +42,8 @@ pub fn resolve_key_alias( keychain: &(dyn KeyStorage + Send + Sync), ) -> napi::Result { if identity_ref.starts_with("did:") { - let did = IdentityDID::new_unchecked(identity_ref.to_string()); + let did = + IdentityDID::parse(identity_ref).map_err(|e| format_error("AUTHS_INVALID_INPUT", e))?; let aliases = keychain .list_aliases_for_identity_with_role(&did, KeyRole::Primary) .map_err(|e| format_error("AUTHS_KEY_NOT_FOUND", format!("Key lookup failed: {e}")))?; diff --git a/packages/auths-node/src/identity.rs b/packages/auths-node/src/identity.rs index eb1f48d7..d57d1e68 100644 --- a/packages/auths-node/src/identity.rs +++ b/packages/auths-node/src/identity.rs @@ -106,6 +106,8 @@ pub fn create_agent_identity( ) -> napi::Result { let passphrase_str = resolve_passphrase(passphrase); let env_config = make_env_config(&passphrase_str, &repo_path); + #[allow(clippy::disallowed_methods)] + // INVARIANT: agent_name is user-provided, format produces valid alias let alias = KeyAlias::new_unchecked(format!("{}-agent", agent_name)); let provider = PrefilledPassphraseProvider::new(&passphrase_str); let clock = Arc::new(SystemClock); @@ -184,6 +186,7 @@ pub fn create_agent_identity( ) })?; + #[allow(clippy::disallowed_methods)] // INVARIANT: device_did from SDK setup result let device_did = DeviceDID::new_unchecked(result.device_did.to_string()); let attestations = attestation_storage .load_attestations_for_device(&device_did) @@ -251,6 +254,8 @@ pub fn delegate_agent( })? }; + #[allow(clippy::disallowed_methods)] + // INVARIANT: agent_name is user-provided, format produces valid alias let agent_alias = KeyAlias::new_unchecked(format!("{}-agent", agent_name)); let rng = SystemRandom::new(); let pkcs8 = Ed25519KeyPair::generate_pkcs8(&rng) @@ -322,6 +327,7 @@ pub fn delegate_agent( ) })?; + #[allow(clippy::disallowed_methods)] // INVARIANT: device_did from SDK setup result let device_did = DeviceDID::new_unchecked(result.device_did.to_string()); let attestations = attestation_storage .load_attestations_for_device(&device_did) @@ -435,7 +441,8 @@ pub fn get_identity_public_key( let keychain = get_platform_keychain_with_config(&env_config) .map_err(|e| format_error("AUTHS_KEYCHAIN_ERROR", format!("Keychain error: {e}")))?; - let did = auths_verifier::types::IdentityDID::new_unchecked(&identity_did); + let did = auths_verifier::types::IdentityDID::parse(&identity_did) + .map_err(|e| format_error("AUTHS_INVALID_INPUT", e))?; let aliases = keychain .list_aliases_for_identity_with_role(&did, KeyRole::Primary) .map_err(|e| format_error("AUTHS_KEY_NOT_FOUND", format!("Key lookup failed: {e}")))?; diff --git a/packages/auths-node/src/org.rs b/packages/auths-node/src/org.rs index 34dd54c2..a654e28f 100644 --- a/packages/auths-node/src/org.rs +++ b/packages/auths-node/src/org.rs @@ -41,7 +41,8 @@ fn find_signer_alias( org_did: &str, keychain: &(dyn auths_core::storage::keychain::KeyStorage + Send + Sync), ) -> napi::Result { - let identity_did = IdentityDID::new_unchecked(org_did.to_string()); + let identity_did = + IdentityDID::parse(org_did).map_err(|e| format_error("AUTHS_ORG_ERROR", e))?; let aliases = keychain .list_aliases_for_identity(&identity_did) .map_err(|e| format_error("AUTHS_ORG_ERROR", e))?; @@ -110,6 +111,7 @@ pub fn create_org( .map_err(|e| format_error("AUTHS_ORG_ERROR", e))?; let backend = Arc::new(backend); + #[allow(clippy::disallowed_methods)] // INVARIANT: key_alias_str from caller input let key_alias = KeyAlias::new_unchecked(key_alias_str); let keychain = get_keychain(&passphrase_str, &repo_path)?; let provider = PrefilledPassphraseProvider::new(&passphrase_str); @@ -143,7 +145,7 @@ pub fn create_org( }; let signer = StorageSigner::new(keychain); - let org_did_device = DeviceDID::new_unchecked(controller_did.to_string()); + let org_did_device = DeviceDID::from_ed25519(org_pk_bytes.as_bytes()); let attestation = create_signed_attestation( now, @@ -219,6 +221,7 @@ pub fn add_org_member( )); let resolver = RegistryDidResolver::new(backend.clone()); + #[allow(clippy::disallowed_methods)] // INVARIANT: hex::encode always produces valid hex let admin_pk_hex = PublicKeyHex::new_unchecked(hex::encode( resolver .resolve(&org_did) @@ -305,6 +308,7 @@ pub fn revoke_org_member( )); let resolver = RegistryDidResolver::new(backend.clone()); + #[allow(clippy::disallowed_methods)] // INVARIANT: hex::encode always produces valid hex let admin_pk_hex = PublicKeyHex::new_unchecked(hex::encode( resolver .resolve(&org_did) diff --git a/packages/auths-node/src/pairing.rs b/packages/auths-node/src/pairing.rs index c34bd391..65342a88 100644 --- a/packages/auths-node/src/pairing.rs +++ b/packages/auths-node/src/pairing.rs @@ -223,6 +223,7 @@ impl NapiPairingHandle { let managed = identity_storage .load_identity() .map_err(|e| format_error("AUTHS_PAIRING_ERROR", e))?; + #[allow(clippy::disallowed_methods)] // INVARIANT: controller_did from storage let controller_identity_did = IdentityDID::new_unchecked(managed.controller_did.to_string()); @@ -234,6 +235,7 @@ impl NapiPairingHandle { .into_iter() .next() .ok_or_else(|| format_error("AUTHS_PAIRING_ERROR", "No primary signing key found"))?; + #[allow(clippy::disallowed_methods)] // INVARIANT: alias from keychain storage let identity_key_alias = KeyAlias::new_unchecked(identity_key_alias_str); let key_storage: Arc = Arc::from(keychain); @@ -312,6 +314,7 @@ pub async fn join_pairing_session( .load_identity() .map_err(|e| format_error("AUTHS_PAIRING_ERROR", e))?; + #[allow(clippy::disallowed_methods)] // INVARIANT: controller_did from storage let controller_identity_did = IdentityDID::new_unchecked(managed.controller_did.to_string()); let keychain = get_keychain(&env_config)?; diff --git a/packages/auths-node/src/sign.rs b/packages/auths-node/src/sign.rs index bfeb268e..492d0a93 100644 --- a/packages/auths-node/src/sign.rs +++ b/packages/auths-node/src/sign.rs @@ -32,7 +32,8 @@ pub fn sign_as_identity( ) -> napi::Result { let passphrase_str = resolve_passphrase(passphrase); let (signer, provider) = make_signer(&passphrase_str, &repo_path)?; - let did = IdentityDID::new_unchecked(&identity_did); + let did = + IdentityDID::parse(&identity_did).map_err(|e| format_error("AUTHS_INVALID_INPUT", e))?; let sig_bytes = signer .sign_for_identity(&did, &provider, message.as_ref()) @@ -85,7 +86,8 @@ pub fn sign_action_as_identity( let passphrase_str = resolve_passphrase(passphrase); let (signer, provider) = make_signer(&passphrase_str, &repo_path)?; - let did = IdentityDID::new_unchecked(&identity_did); + let did = + IdentityDID::parse(&identity_did).map_err(|e| format_error("AUTHS_INVALID_INPUT", e))?; let sig_bytes = signer .sign_for_identity(&did, &provider, canonical.as_bytes()) diff --git a/packages/auths-node/src/trust.rs b/packages/auths-node/src/trust.rs index ea8b85c7..656fbf35 100644 --- a/packages/auths-node/src/trust.rs +++ b/packages/auths-node/src/trust.rs @@ -1,7 +1,9 @@ use std::path::PathBuf; +use std::sync::Arc; use auths_core::trust::pinned::{PinnedIdentity, PinnedIdentityStore, TrustLevel}; -use auths_id::identity::resolve::{DefaultDidResolver, DidResolver}; +use auths_id::identity::resolve::{DefaultDidResolver, DidResolver, RegistryDidResolver}; +use auths_storage::git::{GitRegistryBackend, RegistryConfig}; use auths_verifier::PublicKeyHex; use napi_derive::napi; @@ -60,11 +62,23 @@ pub fn pin_identity( let store = PinnedIdentityStore::new(store_path(&repo_path)); let repo = resolve_repo(&repo_path); - let resolver = DefaultDidResolver::with_repo(&repo); - let public_key_hex = match resolver.resolve(&did) { - Ok(resolved) => PublicKeyHex::new_unchecked(hex::encode(resolved.public_key().as_bytes())), - Err(_) => PublicKeyHex::new_unchecked(""), - }; + let resolved = DefaultDidResolver::with_repo(&repo) + .resolve(&did) + .or_else(|_| { + let backend: Arc = + Arc::new(GitRegistryBackend::from_config_unchecked( + RegistryConfig::single_tenant(&repo), + )); + RegistryDidResolver::new(backend).resolve(&did) + }) + .map_err(|e| { + format_error( + "AUTHS_TRUST_ERROR", + format!("Cannot resolve public key for {did}: {e}"), + ) + })?; + #[allow(clippy::disallowed_methods)] // INVARIANT: hex::encode always produces valid hex + let public_key_hex = PublicKeyHex::new_unchecked(hex::encode(resolved.public_key().as_bytes())); #[allow(clippy::disallowed_methods)] let now = chrono::Utc::now(); diff --git a/packages/auths-node/src/verify.rs b/packages/auths-node/src/verify.rs index 6c7f41d8..555e444c 100644 --- a/packages/auths-node/src/verify.rs +++ b/packages/auths-node/src/verify.rs @@ -129,7 +129,8 @@ pub async fn verify_device_authorization( check_batch_size(&attestations_json)?; let identity_pk_bytes = decode_pk_hex(&identity_pk_hex, "identity public key")?; let attestations = parse_attestations(&attestations_json)?; - let device = DeviceDID::new_unchecked(&device_did); + let device = + DeviceDID::parse(&device_did).map_err(|e| format_error("AUTHS_INVALID_INPUT", e))?; match rust_verify_device_authorization( &identity_did, diff --git a/packages/auths-python/src/attestation_query.rs b/packages/auths-python/src/attestation_query.rs index e6d3d8a9..932ebaff 100644 --- a/packages/auths-python/src/attestation_query.rs +++ b/packages/auths-python/src/attestation_query.rs @@ -6,7 +6,7 @@ use auths_id::storage::attestation::AttestationSource; use auths_storage::git::{GitRegistryBackend, RegistryAttestationStorage, RegistryConfig}; use auths_verifier::core::Attestation; use auths_verifier::types::DeviceDID; -use pyo3::exceptions::PyRuntimeError; +use pyo3::exceptions::{PyRuntimeError, PyValueError}; use pyo3::prelude::*; #[pyclass] @@ -161,7 +161,8 @@ pub fn get_latest_attestation( )) })?; let group = AttestationGroup::from_list(all); - let did = DeviceDID::new_unchecked(device_did.to_string()); + let did = + DeviceDID::parse(device_did).map_err(|e| PyValueError::new_err(format!("{e}")))?; Ok(group.latest(&did).map(attestation_to_py)) }) } diff --git a/packages/auths-python/src/device_ext.rs b/packages/auths-python/src/device_ext.rs index bf57bef0..daf614a6 100644 --- a/packages/auths-python/src/device_ext.rs +++ b/packages/auths-python/src/device_ext.rs @@ -92,7 +92,8 @@ pub fn extend_device_authorization_ffi( let ext_config = DeviceExtensionConfig { repo_path: repo, - device_did: DeviceDID::new_unchecked(device_did), + device_did: DeviceDID::parse(device_did) + .map_err(|e| PyValueError::new_err(format!("{e}")))?, days, identity_key_alias: alias, device_key_alias: None, diff --git a/packages/auths-python/src/identity.rs b/packages/auths-python/src/identity.rs index ab6e7c69..31d9b483 100644 --- a/packages/auths-python/src/identity.rs +++ b/packages/auths-python/src/identity.rs @@ -21,7 +21,7 @@ use auths_storage::git::RegistryIdentityStorage; use auths_verifier::clock::SystemClock; use auths_verifier::core::Capability; use auths_verifier::types::DeviceDID; -use pyo3::exceptions::PyRuntimeError; +use pyo3::exceptions::{PyRuntimeError, PyValueError}; use pyo3::prelude::*; use ring::rand::SystemRandom; use ring::signature::{Ed25519KeyPair, KeyPair}; @@ -52,7 +52,8 @@ pub(crate) fn resolve_key_alias( keychain: &(dyn KeyStorage + Send + Sync), ) -> PyResult { if identity_ref.starts_with("did:") { - let did = IdentityDID::new_unchecked(identity_ref.to_string()); + let did = + IdentityDID::parse(identity_ref).map_err(|e| PyValueError::new_err(format!("{e}")))?; let aliases = keychain .list_aliases_for_identity_with_role(&did, KeyRole::Primary) .map_err(|e| { @@ -205,6 +206,8 @@ pub fn create_agent_identity( ) -> PyResult { let passphrase_str = resolve_passphrase(passphrase); let env_config = make_keychain_config(&passphrase_str, repo_path); + #[allow(clippy::disallowed_methods)] + // INVARIANT: agent_name is user-provided, format produces valid alias let alias = KeyAlias::new_unchecked(format!("{}-agent", agent_name)); let provider = PrefilledPassphraseProvider::new(&passphrase_str); @@ -360,6 +363,8 @@ pub fn delegate_agent( }; // Generate a new Ed25519 keypair for the agent + #[allow(clippy::disallowed_methods)] + // INVARIANT: agent_name is user-provided, format produces valid alias let agent_alias = KeyAlias::new_unchecked(format!("{}-agent", agent_name)); let rng = SystemRandom::new(); let pkcs8 = Ed25519KeyPair::generate_pkcs8(&rng).map_err(|e| { @@ -439,6 +444,7 @@ pub fn delegate_agent( )) })?; + #[allow(clippy::disallowed_methods)] // INVARIANT: device_did from SDK setup result let device_did = DeviceDID::new_unchecked(result.device_did.to_string()); let attestations = attestation_storage .load_attestations_for_device(&device_did) diff --git a/packages/auths-python/src/identity_sign.rs b/packages/auths-python/src/identity_sign.rs index a9ddeae8..8ba4a021 100644 --- a/packages/auths-python/src/identity_sign.rs +++ b/packages/auths-python/src/identity_sign.rs @@ -3,7 +3,7 @@ use auths_core::signing::{PrefilledPassphraseProvider, SecureSigner, StorageSign use auths_core::storage::keychain::{KeyAlias, KeyRole, get_platform_keychain_with_config}; use auths_verifier::core::MAX_ATTESTATION_JSON_SIZE; use auths_verifier::types::IdentityDID; -use pyo3::exceptions::PyRuntimeError; +use pyo3::exceptions::{PyRuntimeError, PyValueError}; use pyo3::prelude::*; fn make_signer( @@ -58,7 +58,8 @@ pub fn sign_as_identity( passphrase: Option, ) -> PyResult { let (signer, provider) = make_signer(Some(repo_path), passphrase)?; - let did = IdentityDID::new_unchecked(identity_did); + let did = + IdentityDID::parse(identity_did).map_err(|e| PyValueError::new_err(format!("{e}")))?; let msg = message.to_vec(); py.allow_threads(move || { @@ -123,7 +124,8 @@ pub fn sign_action_as_identity( })?; let (signer, provider) = make_signer(Some(repo_path), passphrase)?; - let did = IdentityDID::new_unchecked(identity_did); + let did = + IdentityDID::parse(identity_did).map_err(|e| PyValueError::new_err(format!("{e}")))?; let action_type_owned = action_type.to_string(); let identity_did_owned = identity_did.to_string(); @@ -173,7 +175,8 @@ pub fn get_identity_public_key( passphrase: Option, ) -> PyResult { let (signer, provider) = make_signer(Some(repo_path), passphrase)?; - let did = IdentityDID::new_unchecked(identity_did); + let did = + IdentityDID::parse(identity_did).map_err(|e| PyValueError::new_err(format!("{e}")))?; py.allow_threads(move || { let aliases = signer diff --git a/packages/auths-python/src/org.rs b/packages/auths-python/src/org.rs index 57f757a1..9216005c 100644 --- a/packages/auths-python/src/org.rs +++ b/packages/auths-python/src/org.rs @@ -41,7 +41,8 @@ fn find_signer_alias( org_did: &str, keychain: &(dyn auths_core::storage::keychain::KeyStorage + Send + Sync), ) -> PyResult { - let identity_did = IdentityDID::new_unchecked(org_did.to_string()); + let identity_did = IdentityDID::parse(org_did) + .map_err(|e| PyRuntimeError::new_err(format!("[AUTHS_ORG_ERROR] {e}")))?; let aliases = keychain .list_aliases_for_identity(&identity_did) .map_err(|e| PyRuntimeError::new_err(format!("[AUTHS_ORG_ERROR] {e}")))?; @@ -94,6 +95,7 @@ pub fn create_org( .map_err(|e| PyRuntimeError::new_err(format!("[AUTHS_ORG_ERROR] {e}")))?; let backend = Arc::new(backend); + #[allow(clippy::disallowed_methods)] // INVARIANT: key_alias_str from caller input let key_alias = KeyAlias::new_unchecked(key_alias_str); let keychain = get_keychain(&passphrase_str, &repo_path_str)?; let provider = auths_core::signing::PrefilledPassphraseProvider::new(&passphrase_str); @@ -127,7 +129,7 @@ pub fn create_org( }; let signer = StorageSigner::new(keychain); - let org_did_device = DeviceDID::new_unchecked(controller_did.to_string()); + let org_did_device = DeviceDID::from_ed25519(org_pk_bytes.as_bytes()); let attestation = create_signed_attestation( now, @@ -205,6 +207,7 @@ pub fn add_org_member( )); let resolver = RegistryDidResolver::new(backend.clone()); + #[allow(clippy::disallowed_methods)] // INVARIANT: hex::encode always produces valid hex let admin_pk_hex = PublicKeyHex::new_unchecked(hex::encode( resolver .resolve(&org_did) @@ -299,6 +302,7 @@ pub fn revoke_org_member( )); let resolver = RegistryDidResolver::new(backend.clone()); + #[allow(clippy::disallowed_methods)] // INVARIANT: hex::encode always produces valid hex let admin_pk_hex = PublicKeyHex::new_unchecked(hex::encode( resolver .resolve(&org_did) diff --git a/packages/auths-python/src/pairing.rs b/packages/auths-python/src/pairing.rs index ed59d1b0..b39be3a3 100644 --- a/packages/auths-python/src/pairing.rs +++ b/packages/auths-python/src/pairing.rs @@ -230,6 +230,7 @@ pub fn join_pairing_session_ffi( .load_identity() .map_err(|e| PyRuntimeError::new_err(format!("[AUTHS_PAIRING_ERROR] {e}")))?; + #[allow(clippy::disallowed_methods)] // INVARIANT: controller_did from storage let controller_identity_did = IdentityDID::new_unchecked(managed.controller_did.to_string()); @@ -402,6 +403,7 @@ pub fn complete_pairing_ffi( let managed = identity_storage .load_identity() .map_err(|e| PyRuntimeError::new_err(format!("[AUTHS_PAIRING_ERROR] {e}")))?; + #[allow(clippy::disallowed_methods)] // INVARIANT: controller_did from storage let controller_identity_did = IdentityDID::new_unchecked(managed.controller_did.to_string()); @@ -412,6 +414,7 @@ pub fn complete_pairing_ffi( let identity_key_alias_str = aliases.into_iter().next().ok_or_else(|| { PyRuntimeError::new_err("[AUTHS_PAIRING_ERROR] No primary signing key found") })?; + #[allow(clippy::disallowed_methods)] // INVARIANT: alias from keychain storage let identity_key_alias = KeyAlias::new_unchecked(identity_key_alias_str); let key_storage: Arc = diff --git a/packages/auths-python/src/trust.rs b/packages/auths-python/src/trust.rs index ea3fd170..3aa2b2d3 100644 --- a/packages/auths-python/src/trust.rs +++ b/packages/auths-python/src/trust.rs @@ -1,9 +1,11 @@ use pyo3::exceptions::{PyRuntimeError, PyValueError}; use pyo3::prelude::*; use std::path::PathBuf; +use std::sync::Arc; use auths_core::trust::pinned::{PinnedIdentity, PinnedIdentityStore, TrustLevel}; -use auths_id::identity::resolve::{DefaultDidResolver, DidResolver}; +use auths_id::identity::resolve::{DefaultDidResolver, DidResolver, RegistryDidResolver}; +use auths_storage::git::{GitRegistryBackend, RegistryConfig}; use auths_verifier::PublicKeyHex; use chrono::Utc; @@ -53,17 +55,23 @@ pub fn pin_identity( let store = PinnedIdentityStore::new(store_path(&repo)); let repo_path = resolve_repo(&repo); - let resolver = DefaultDidResolver::with_repo(&repo_path); - let public_key_hex = match resolver.resolve(&did) { - Ok(resolved) => { - PublicKeyHex::new_unchecked(hex::encode(resolved.public_key().as_bytes())) - } - Err(_) => { - // If DID can't be resolved, use a placeholder — the pin still works - // for trust-on-first-use patterns where the key isn't known yet - PublicKeyHex::new_unchecked("") - } - }; + let resolved = DefaultDidResolver::with_repo(&repo_path) + .resolve(&did) + .or_else(|_| { + let backend: Arc = + Arc::new(GitRegistryBackend::from_config_unchecked( + RegistryConfig::single_tenant(&repo_path), + )); + RegistryDidResolver::new(backend).resolve(&did) + }) + .map_err(|e| { + PyRuntimeError::new_err(format!( + "[AUTHS_TRUST_ERROR] Cannot resolve public key for {did}: {e}" + )) + })?; + #[allow(clippy::disallowed_methods)] // INVARIANT: hex::encode always produces valid hex + let public_key_hex = + PublicKeyHex::new_unchecked(hex::encode(resolved.public_key().as_bytes())); // Check if already pinned — if so, update label by remove + re-pin if let Ok(Some(existing)) = store.lookup(&did) { diff --git a/packages/auths-python/src/verify.rs b/packages/auths-python/src/verify.rs index 565c81fb..391f3ff2 100644 --- a/packages/auths-python/src/verify.rs +++ b/packages/auths-python/src/verify.rs @@ -178,7 +178,7 @@ pub fn verify_device_authorization( }) .collect::>>()?; - let device = DeviceDID::new_unchecked(device_did); + let device = DeviceDID::parse(device_did).map_err(|e| PyValueError::new_err(format!("{e}")))?; py.allow_threads(|| { match runtime().block_on(rust_verify_device_authorization( diff --git a/packages/auths-verifier-swift/src/lib.rs b/packages/auths-verifier-swift/src/lib.rs index 643562f4..4108ea78 100644 --- a/packages/auths-verifier-swift/src/lib.rs +++ b/packages/auths-verifier-swift/src/lib.rs @@ -287,7 +287,8 @@ pub fn verify_device_authorization( }) .collect::, _>>()?; - let device = DeviceDID::new_unchecked(&device_did); + let device = DeviceDID::parse(&device_did) + .map_err(|e| VerifierError::InvalidInput(format!("Invalid device DID: {e}")))?; // Verify match rust_verify_device_authorization(&identity_did, &device, &attestations, &identity_pk_bytes) {