Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ jobs:
key: ${{ runner.os }}-clippy-${{ hashFiles('**/Cargo.lock') }}
restore-keys: ${{ runner.os }}-clippy-
- run: cargo clippy --all-targets --all-features -- -D warnings
- run: cargo run -p xtask -- check-clippy-sync

schemas:
name: Schema validation
Expand Down
7 changes: 7 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,13 @@ repos:
files: (crates/auths-cli/src/|crates/xtask/src/gen_docs|docs/cli/commands/)
pass_filenames: false

- id: check-clippy-sync
name: cargo xtask check-clippy-sync
entry: cargo run --package xtask -- check-clippy-sync
language: system
files: clippy\.toml$
pass_filenames: false

- id: cargo-deny
name: cargo deny (licenses + bans)
entry: bash -c 'cargo deny check > .cargo/cargo-deny.log 2>&1; exit $?'
Expand Down
4 changes: 2 additions & 2 deletions crates/auths-cli/src/commands/device/authorization.rs
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,7 @@ pub fn handle_device(
&ctx,
&auths_core::ports::clock::SystemClock,
)
.map_err(|e| anyhow!("{e}"))?;
.map_err(anyhow::Error::new)?;

display_link_result(&result, &device_did)
}
Expand Down Expand Up @@ -301,7 +301,7 @@ pub fn handle_device(
note,
&auths_core::ports::clock::SystemClock,
)
.map_err(|e| anyhow!("{e}"))?;
.map_err(anyhow::Error::new)?;

display_revoke_result(&device_did, &repo_path)
}
Expand Down
5 changes: 3 additions & 2 deletions crates/auths-cli/src/commands/init/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -437,7 +437,8 @@ mod tests {
registry: DEFAULT_REGISTRY_URL.to_string(),
skip_registration: false,
};
// In test context, stdin is not a TTY
assert!(!resolve_interactive(&cmd).unwrap());
// Auto-detect returns is_terminal() — result depends on environment
let result = resolve_interactive(&cmd).unwrap();
assert_eq!(result, std::io::stdin().is_terminal());
}
}
4 changes: 4 additions & 0 deletions crates/auths-cli/src/commands/verify_commit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -919,6 +919,8 @@ mod tests {

#[tokio::test]
async fn verify_bundle_chain_empty_chain() {
#[allow(clippy::disallowed_methods)]
// INVARIANT: test-only hardcoded DID and hex string literals
let bundle = IdentityBundle {
identity_did: auths_verifier::IdentityDID::new_unchecked("did:keri:test"),
public_key_hex: auths_verifier::PublicKeyHex::new_unchecked("aa".repeat(32)),
Expand All @@ -935,6 +937,8 @@ mod tests {

#[tokio::test]
async fn verify_bundle_chain_invalid_hex() {
#[allow(clippy::disallowed_methods)]
// INVARIANT: test-only hardcoded DID, hex, and canonical DID string literals
let bundle = IdentityBundle {
identity_did: auths_verifier::IdentityDID::new_unchecked("did:keri:test"),
public_key_hex: auths_verifier::PublicKeyHex::new_unchecked("not_hex"),
Expand Down
46 changes: 28 additions & 18 deletions crates/auths-cli/src/errors/renderer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,13 @@ pub fn render_error(err: &Error, json_mode: bool) {
}

/// Try to extract `AuthsErrorInfo` from an `anyhow::Error` by downcasting
/// through all known error types.
/// through all known error types. Walks the full error chain so that
/// `.with_context()` wrapping doesn't hide typed errors.
fn extract_error_info(err: &Error) -> Option<(&str, &str, Option<&str>)> {
macro_rules! try_downcast {
($err:expr, $($ty:ty),+ $(,)?) => {
($source:expr, $($ty:ty),+ $(,)?) => {
$(
if let Some(e) = $err.downcast_ref::<$ty>() {
if let Some(e) = $source.downcast_ref::<$ty>() {
let code = AuthsErrorInfo::error_code(e);
let msg = format!("{e}");
// SAFETY: we leak the String to get a &'static str because
Expand All @@ -46,21 +47,23 @@ fn extract_error_info(err: &Error) -> Option<(&str, &str, Option<&str>)> {
};
}

try_downcast!(
err,
AgentError,
AttestationError,
SetupError,
DeviceError,
DeviceExtensionError,
RotationError,
RegistrationError,
McpAuthError,
OrgError,
ApprovalError,
AllowedSignersError,
SigningError,
);
for cause in err.chain() {
try_downcast!(
cause,
AgentError,
AttestationError,
SetupError,
DeviceError,
DeviceExtensionError,
RotationError,
RegistrationError,
McpAuthError,
OrgError,
ApprovalError,
AllowedSignersError,
SigningError,
);
}

None
}
Expand Down Expand Up @@ -248,4 +251,11 @@ mod tests {
assert_eq!(code, "AUTHS-E3001");
assert!(suggestion.is_some());
}

#[test]
fn extract_error_info_walks_chain_through_context() {
let err: Error = Error::new(AgentError::KeyNotFound).context("operation failed");
let (code, _, _) = extract_error_info(&err).unwrap();
assert_eq!(code, "AUTHS-E3001");
}
}
7 changes: 7 additions & 0 deletions crates/auths-core/clippy.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@ 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/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 },

# === Sans-IO: filesystem ===
{ path = "std::fs::read", reason = "sans-IO crate — use a port trait" },
{ path = "std::fs::read_to_string", reason = "sans-IO crate — use a port trait" },
Expand Down
2 changes: 2 additions & 0 deletions crates/auths-core/src/api/ffi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,8 @@ pub unsafe extern "C" fn ffi_import_key(
}
};

#[allow(clippy::disallowed_methods)]
// INVARIANT: validated with starts_with("did:") guard above
let did_string = IdentityDID::new_unchecked(did_str.to_string());
let alias = KeyAlias::new_unchecked(alias_str);

Expand Down
38 changes: 27 additions & 11 deletions crates/auths-core/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -174,16 +174,27 @@ impl AuthsErrorInfo for AgentError {
Some("Run the command again and provide the required input")
}
Self::StorageError(_) => Some("Check file permissions and disk space"),
// These errors typically don't have actionable suggestions
Self::SecurityError(_)
| Self::CryptoError(_)
| Self::KeyDeserializationError(_)
| Self::SigningFailed(_)
| Self::Proto(_)
| Self::IO(_)
| Self::InvalidInput(_)
| Self::MutexError(_)
| Self::CredentialTooLarge { .. } => None,
Self::SecurityError(_) => Some(
"Run `auths doctor` to check system keychain access and security configuration",
),
Self::CryptoError(_) => {
Some("A cryptographic operation failed; check key material with `auths key list`")
}
Self::KeyDeserializationError(_) => {
Some("The stored key is corrupted; re-import with `auths key import`")
}
Self::SigningFailed(_) => Some(
"The signing operation failed; verify your key is accessible with `auths key list`",
),
Self::Proto(_) => Some(
"A protocol error occurred; check that both sides are running compatible versions",
),
Self::IO(_) => Some("Check file permissions and that the filesystem is not read-only"),
Self::InvalidInput(_) => Some("Check the command arguments and try again"),
Self::MutexError(_) => Some("A concurrency error occurred; restart the operation"),
Self::CredentialTooLarge { .. } => Some(
"Reduce the credential size or use file-based storage with AUTHS_KEYCHAIN_BACKEND=file",
),
Self::WeakPassphrase(_) => {
Some("Use at least 12 characters with uppercase, lowercase, and a digit or symbol")
}
Expand Down Expand Up @@ -244,7 +255,12 @@ impl AuthsErrorInfo for TrustError {
Self::Lock(_) => Some("Check file permissions and try again"),
Self::Io(_) => Some("Check disk space and file permissions"),
Self::AlreadyExists(_) => Some("Run `auths trust list` to see existing entries"),
Self::InvalidData(_) | Self::Serialization(_) => None,
Self::InvalidData(_) => {
Some("The trust store may be corrupted; delete and re-pin with `auths trust add`")
}
Self::Serialization(_) => {
Some("The trust store data is corrupted; delete and re-pin with `auths trust add`")
}
}
}
}
Expand Down
11 changes: 10 additions & 1 deletion crates/auths-core/src/pairing/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,16 @@ impl AuthsErrorInfo for PairingError {
Self::NoPeerFound => Some("Ensure both devices are on the same network"),
Self::LanTimeout => Some("Check your network and try again"),
Self::RelayError(_) => Some("Check your internet connection"),
_ => None,
Self::Protocol(_) => Some("Ensure both devices are running compatible auths versions"),
Self::QrCodeFailed(_) => {
Some("QR code generation failed; try `auths device pair --mode relay` instead")
}
Self::LocalServerError(_) => {
Some("The local pairing server failed to start; check that the port is available")
}
Self::MdnsError(_) => {
Some("mDNS discovery failed; try `auths device pair --mode relay` instead")
}
}
}
}
Expand Down
10 changes: 9 additions & 1 deletion crates/auths-core/src/ports/network.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,15 @@ impl auths_crypto::AuthsErrorInfo for NetworkError {
Self::Unreachable { .. } => Some("Check your internet connection"),
Self::Timeout { .. } => Some("The server may be overloaded — retry later"),
Self::Unauthorized => Some("Check your authentication credentials"),
_ => None,
Self::NotFound { .. } => Some(
"The requested resource was not found on the server; verify the URL or identifier",
),
Self::InvalidResponse { .. } => {
Some("The server returned an unexpected response; check server compatibility")
}
Self::Internal(_) => Some(
"The server encountered an internal error; retry later or contact the server administrator",
),
}
}
}
Expand Down
10 changes: 9 additions & 1 deletion crates/auths-core/src/ports/platform.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,15 @@ impl auths_crypto::AuthsErrorInfo for PlatformError {
Self::AccessDenied => Some("Re-run the command and approve the authorization request"),
Self::ExpiredToken => Some("The device code expired — restart the flow"),
Self::Network(_) => Some("Check your internet connection"),
_ => None,
Self::AuthorizationPending => Some(
"Complete the authorization on the linked device, then the CLI will continue automatically",
),
Self::SlowDown => {
Some("The authorization server is rate-limiting; the CLI will retry automatically")
}
Self::Platform { .. } => {
Some("A platform-specific error occurred; run `auths doctor` to diagnose")
}
}
}
}
Expand Down
6 changes: 6 additions & 0 deletions crates/auths-core/src/signing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -782,6 +782,8 @@ mod tests {
fn test_sign_for_identity_success() {
let (pkcs8_bytes, pubkey_bytes) = generate_test_keypair();
let passphrase = "Test-P@ss12345";
#[allow(clippy::disallowed_methods)]
// INVARIANT: test-only literal with valid did:keri: prefix
let identity_did = IdentityDID::new_unchecked("did:keri:ABC123");
let alias = KeyAlias::new_unchecked("test-key-alias");

Expand Down Expand Up @@ -815,6 +817,8 @@ mod tests {
let signer = StorageSigner::new(storage);
let passphrase_provider = MockPassphraseProvider::new("any-passphrase");

#[allow(clippy::disallowed_methods)]
// INVARIANT: test-only literal with valid did:keri: prefix
let identity_did = IdentityDID::new_unchecked("did:keri:NONEXISTENT");
let message = b"test message";

Expand All @@ -827,6 +831,8 @@ mod tests {
// Test that sign_for_identity works when multiple aliases exist for an identity
let (pkcs8_bytes, pubkey_bytes) = generate_test_keypair();
let passphrase = "Test-P@ss12345";
#[allow(clippy::disallowed_methods)]
// INVARIANT: test-only literal with valid did:keri: prefix
let identity_did = IdentityDID::new_unchecked("did:keri:MULTI123");

let encrypted = encrypt_keypair(&pkcs8_bytes, passphrase).expect("Failed to encrypt");
Expand Down
4 changes: 4 additions & 0 deletions crates/auths-core/src/storage/ios_keychain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,8 @@ impl KeyStorage for IOSKeychain {
.unwrap_or(KeyRole::Primary);

debug!("Successfully loaded key for alias '{}'", alias);
#[allow(clippy::disallowed_methods)]
// INVARIANT: loaded from iOS Keychain which stores validated DIDs
Ok((IdentityDID::new_unchecked(identity_did_str), role, key_data))
}

Expand Down Expand Up @@ -450,6 +452,8 @@ impl KeyStorage for IOSKeychain {
})?;

debug!("Found identity DID for alias '{}'", alias);
#[allow(clippy::disallowed_methods)]
// INVARIANT: loaded from iOS Keychain which stores validated DIDs
Ok(IdentityDID::new_unchecked(identity_did))
}

Expand Down
2 changes: 2 additions & 0 deletions crates/auths-core/src/storage/keychain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -619,6 +619,8 @@ mod tests {
use super::super::memory::IsolatedKeychainHandle;

let keychain = IsolatedKeychainHandle::new();
#[allow(clippy::disallowed_methods)]
// INVARIANT: test-only literal with valid did:keri: prefix
let did = IdentityDID::new_unchecked("did:keri:Etest".to_string());

keychain
Expand Down
4 changes: 4 additions & 0 deletions crates/auths-core/src/storage/linux_secret_service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,8 @@ impl KeyStorage for LinuxSecretServiceStorage {
3 => {
let role = parts[1].parse::<KeyRole>().unwrap_or(KeyRole::Primary);
(
#[allow(clippy::disallowed_methods)]
// INVARIANT: DID was stored by this keychain impl, already validated on write
IdentityDID::new_unchecked(parts[0].to_string()),
role,
parts[2],
Expand All @@ -218,6 +220,8 @@ impl KeyStorage for LinuxSecretServiceStorage {
2 => {
// Legacy format: did|base64_key_data
(
#[allow(clippy::disallowed_methods)]
// INVARIANT: DID was stored by this keychain impl, already validated on write
IdentityDID::new_unchecked(parts[0].to_string()),
KeyRole::Primary,
parts[1],
Expand Down
4 changes: 4 additions & 0 deletions crates/auths-core/src/storage/macos_keychain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,8 @@ impl KeyStorage for MacOSKeychain {
.unwrap_or(KeyRole::Primary);

debug!("Successfully loaded key for alias '{}'", alias);
#[allow(clippy::disallowed_methods)]
// INVARIANT: loaded from macOS Keychain which stores validated DIDs
Ok((IdentityDID::new_unchecked(identity_did_str), role, key_data))
}

Expand Down Expand Up @@ -659,6 +661,8 @@ impl KeyStorage for MacOSKeychain {
}

debug!("Found identity DID for alias '{}'", alias);
#[allow(clippy::disallowed_methods)]
// INVARIANT: loaded from macOS Keychain which stores validated DIDs
Ok(IdentityDID::new_unchecked(identity_did_str))
}

Expand Down
4 changes: 4 additions & 0 deletions crates/auths-core/src/storage/pkcs11.rs
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,8 @@ impl KeyStorage for Pkcs11KeyRef {
})
.ok_or(AgentError::KeyNotFound)?;

#[allow(clippy::disallowed_methods)]
// INVARIANT: loaded from PKCS#11 token which stores validated DIDs
let identity_did = IdentityDID::new_unchecked(
String::from_utf8(id_bytes)
.map_err(|e| AgentError::KeyDeserializationError(e.to_string()))?,
Expand Down Expand Up @@ -317,6 +319,8 @@ impl KeyStorage for Pkcs11KeyRef {
})
.ok_or(AgentError::KeyNotFound)?;

#[allow(clippy::disallowed_methods)]
// INVARIANT: loaded from PKCS#11 token which stores validated DIDs
Ok(IdentityDID::new_unchecked(
String::from_utf8(id_bytes)
.map_err(|e| AgentError::KeyDeserializationError(e.to_string()))?,
Expand Down
2 changes: 2 additions & 0 deletions crates/auths-core/src/trust/resolve.rs
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ pub fn resolve_trust(
if prompt(&msg) {
let pin = PinnedIdentity {
did,
#[allow(clippy::disallowed_methods)] // INVARIANT: hex::encode on line 151 guarantees valid hex output
public_key_hex: PublicKeyHex::new_unchecked(pk_hex),
kel_tip_said: None,
kel_sequence: None,
Expand Down Expand Up @@ -188,6 +189,7 @@ pub fn resolve_trust(
TrustDecision::RotationVerified { old_pin, proof } => {
let updated = PinnedIdentity {
did: old_pin.did,
#[allow(clippy::disallowed_methods)] // INVARIANT: hex::encode always produces valid lowercase hex
public_key_hex: PublicKeyHex::new_unchecked(hex::encode(&proof.new_public_key)),
kel_tip_said: Some(proof.new_kel_tip),
kel_sequence: Some(proof.new_sequence),
Expand Down
Loading
Loading