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 Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 13 additions & 0 deletions clippy.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,19 @@ disallowed-methods = [
{ 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 },

# === Curve-agnostic refactor (fn-114) — ban Ed25519-hardcoded APIs ===
# Removed in fn-114.40 cleanup after all sweeps complete.
{ path = "ring::signature::Ed25519KeyPair::from_pkcs8", reason = "use TypedSignerKey::from_pkcs8 — dispatches on curve.", allow-invalid = true },
{ path = "ring::signature::Ed25519KeyPair::from_seed_unchecked", reason = "use auths_crypto::sign(&TypedSeed, msg).", allow-invalid = true },
{ path = "ring::signature::Ed25519KeyPair::generate_pkcs8", reason = "use inception::generate_keypair_for_init(curve).", allow-invalid = true },
{ path = "ring::signature::UnparsedPublicKey::new", reason = "use DevicePublicKey::verify — dispatches on curve.", allow-invalid = true },
{ path = "auths_crypto::parse_ed25519_seed", reason = "use parse_key_material — returns TypedSeed.", allow-invalid = true },
{ path = "auths_crypto::parse_ed25519_key_material", reason = "use parse_key_material — returns TypedSeed.", allow-invalid = true },
{ path = "auths_core::crypto::provider_bridge::sign_ed25519_sync", reason = "use auths_crypto::sign(&TypedSeed, msg).", allow-invalid = true },
{ path = "auths_core::crypto::provider_bridge::ed25519_public_key_from_seed_sync", reason = "use auths_crypto::public_key(&TypedSeed) or TypedSignerKey::public_key().", allow-invalid = true },
{ path = "auths_crypto::did_key_to_ed25519", reason = "use did_key_decode — returns DecodedDidKey.", allow-invalid = true },
{ path = "auths_id::identity::resolve::ed25519_to_did_key", reason = "use did_key_decode + DecodedDidKey::P256/Ed25519.", allow-invalid = true },
]
allow-unwrap-in-tests = true
allow-expect-in-tests = true
13 changes: 13 additions & 0 deletions crates/auths-cli/clippy.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,17 @@ disallowed-methods = [
{ 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 },

# === Curve-agnostic refactor (fn-114) — ban Ed25519-hardcoded APIs ===
# Removed in fn-114.40 cleanup after all sweeps complete.
{ path = "ring::signature::Ed25519KeyPair::from_pkcs8", reason = "use TypedSignerKey::from_pkcs8 — dispatches on curve.", allow-invalid = true },
{ path = "ring::signature::Ed25519KeyPair::from_seed_unchecked", reason = "use auths_crypto::sign(&TypedSeed, msg).", allow-invalid = true },
{ path = "ring::signature::Ed25519KeyPair::generate_pkcs8", reason = "use inception::generate_keypair_for_init(curve).", allow-invalid = true },
{ path = "ring::signature::UnparsedPublicKey::new", reason = "use DevicePublicKey::verify — dispatches on curve.", allow-invalid = true },
{ path = "auths_crypto::parse_ed25519_seed", reason = "use parse_key_material — returns TypedSeed.", allow-invalid = true },
{ path = "auths_crypto::parse_ed25519_key_material", reason = "use parse_key_material — returns TypedSeed.", allow-invalid = true },
{ path = "auths_core::crypto::provider_bridge::sign_ed25519_sync", reason = "use auths_crypto::sign(&TypedSeed, msg).", allow-invalid = true },
{ path = "auths_core::crypto::provider_bridge::ed25519_public_key_from_seed_sync", reason = "use auths_crypto::public_key(&TypedSeed) or TypedSignerKey::public_key().", allow-invalid = true },
{ path = "auths_crypto::did_key_to_ed25519", reason = "use did_key_decode — returns DecodedDidKey.", allow-invalid = true },
{ path = "auths_id::identity::resolve::ed25519_to_did_key", reason = "use did_key_decode + DecodedDidKey::P256/Ed25519.", allow-invalid = true },
]
2 changes: 2 additions & 0 deletions crates/auths-cli/src/bin/sign.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
#![allow(clippy::print_stdout, clippy::print_stderr, clippy::exit)]
// allow during curve-agnostic refactor
#![allow(clippy::disallowed_methods)]
//! auths-sign: Git SSH signing program compatible with `gpg.ssh.program`
//!
//! Git calls this binary with ssh-keygen compatible arguments:
Expand Down
2 changes: 2 additions & 0 deletions crates/auths-cli/src/bin/verify.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// allow during curve-agnostic refactor
#![allow(clippy::disallowed_methods)]
#![allow(clippy::print_stdout, clippy::print_stderr, clippy::exit)]
//! auths-verify: SSH signature verification for Auths identities
//!
Expand Down
36 changes: 19 additions & 17 deletions crates/auths-cli/src/commands/artifact/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -345,23 +345,25 @@ pub fn handle_artifact(
None => bail!("--ci requires --commit <sha>. Pass the commit SHA explicitly."),
};

let ci_env = match detect_ci_environment() {
Some(env) => env,
None => match ci_platform.as_deref() {
Some("local") => CiEnvironment {
platform: CiPlatform::Local,
workflow_ref: None,
run_id: None,
actor: None,
runner_os: None,
},
Some(name) => CiEnvironment {
platform: CiPlatform::Generic,
workflow_ref: None,
run_id: None,
actor: None,
runner_os: Some(name.to_string()),
},
// Explicit --ci-platform takes precedence over auto-detection so
// tests can opt out of the CI runner's auto-detected platform.
let ci_env = match ci_platform.as_deref() {
Some("local") => CiEnvironment {
platform: CiPlatform::Local,
workflow_ref: None,
run_id: None,
actor: None,
runner_os: None,
},
Some(name) => CiEnvironment {
platform: CiPlatform::Generic,
workflow_ref: None,
run_id: None,
actor: None,
runner_os: Some(name.to_string()),
},
None => match detect_ci_environment() {
Some(env) => env,
None => bail!(
"No CI environment detected. If this is intentional (e.g., testing), \
pass --ci-platform local. Otherwise run inside GitHub Actions, \
Expand Down
31 changes: 29 additions & 2 deletions crates/auths-cli/src/commands/device/authorization.rs
Original file line number Diff line number Diff line change
Expand Up @@ -491,8 +491,35 @@ fn list_devices(
.last()
.expect("Grouped attestations should not be empty");

let verification_result =
auths_sdk::attestation::verify_with_resolver(now, &resolver, latest, None);
// single verifier path via auths_verifier::verify_with_keys.
// Callers resolve the DID and pass the typed key directly.
let verification_result: Result<(), auths_verifier::AttestationError> = {
use auths_sdk::identity::DidResolver;
use auths_verifier::AttestationError;
match resolver.resolve(latest.issuer.as_str()) {
Ok(resolved) => {
let pk_bytes: Vec<u8> = resolved.public_key_bytes().to_vec();
match auths_verifier::decode_public_key_bytes(&pk_bytes) {
Ok(issuer_pk) => {
#[allow(clippy::expect_used)]
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("tokio runtime");
rt.block_on(auths_verifier::verify_with_keys(latest, &issuer_pk))
.map(|_| ())
}
Err(e) => Err(AttestationError::DidResolutionError(format!(
"invalid issuer key: {e}"
))),
}
}
Err(e) => Err(AttestationError::DidResolutionError(format!(
"Resolver error for {}: {}",
latest.issuer, e
))),
}
};

let status_string = match verification_result {
Ok(()) => {
Expand Down
1 change: 1 addition & 0 deletions crates/auths-cli/src/commands/device/verify_attestation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,7 @@ fn resolve_issuer_key(
let pin = PinnedIdentity {
did: did.to_string(),
public_key_hex: root.public_key_hex.clone(),
curve: root.curve,
kel_tip_said: root.kel_tip_said.clone(),
kel_sequence: None,
first_seen: now,
Expand Down
46 changes: 33 additions & 13 deletions crates/auths-cli/src/commands/org.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use serde_json;
use std::fs;
use std::path::PathBuf;

use auths_sdk::attestation::{AttestationGroup, AttestationSink, verify_with_resolver};
use auths_sdk::attestation::{AttestationGroup, AttestationSink};
use auths_sdk::identity::DefaultDidResolver;
use auths_sdk::keychain::{KeyAlias, get_platform_keychain};
use auths_sdk::ports::{AttestationMetadata, AttestationSource, IdentityStorage};
Expand Down Expand Up @@ -194,6 +194,36 @@ pub enum OrgSubcommand {
},
}

/// single-verifier helper. Resolves the issuer DID,
/// constructs a typed `DevicePublicKey`, and calls `auths_verifier::verify_with_keys`.
/// Returns one of: "✅ valid", "🛑 revoked", "⌛ expired", "❌ invalid".
fn verify_attestation_via_resolver(
att: &auths_verifier::Attestation,
resolver: &auths_sdk::identity::DefaultDidResolver,
) -> &'static str {
use auths_sdk::identity::DidResolver;
let resolved = match resolver.resolve(att.issuer.as_str()) {
Ok(r) => r,
Err(_) => return "❌ invalid",
};
let pk_bytes: Vec<u8> = resolved.public_key_bytes().to_vec();
let issuer_pk = match auths_verifier::decode_public_key_bytes(&pk_bytes) {
Ok(pk) => pk,
Err(_) => return "❌ invalid",
};
#[allow(clippy::expect_used)]
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("tokio runtime");
match rt.block_on(auths_verifier::verify_with_keys(att, &issuer_pk)) {
Ok(_) => "✅ valid",
Err(e) if e.to_string().contains("revoked") => "🛑 revoked",
Err(e) if e.to_string().contains("expired") => "⌛ expired",
Err(_) => "❌ invalid",
}
}

/// Handles `org` commands for issuing or revoking member authorizations.
pub fn handle_org(
cmd: OrgCommand,
Expand Down Expand Up @@ -589,12 +619,7 @@ pub fn handle_org(
continue;
}

let status = match verify_with_resolver(now, &resolver, att, None) {
Ok(_) => "✅ valid",
Err(e) if e.to_string().contains("revoked") => "🛑 revoked",
Err(e) if e.to_string().contains("expired") => "⌛ expired",
Err(_) => "❌ invalid",
};
let status = verify_attestation_via_resolver(att, &resolver);

println!("{i}. [{}] @ {}", status, att.timestamp.unwrap_or(now));
if let Some(note) = &att.note {
Expand Down Expand Up @@ -626,12 +651,7 @@ pub fn handle_org(
continue;
}

let status = match verify_with_resolver(now, &resolver, latest, None) {
Ok(_) => "✅ valid",
Err(e) if e.to_string().contains("revoked") => "🛑 revoked",
Err(e) if e.to_string().contains("expired") => "⌛ expired",
Err(_) => "❌ invalid",
};
let status = verify_attestation_via_resolver(latest, &resolver);

println!("- {} [{}]", subject, status);
}
Expand Down
8 changes: 7 additions & 1 deletion crates/auths-cli/src/commands/trust.rs
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,13 @@ fn handle_pin(cmd: TrustPinCommand, now: DateTime<Utc>) -> Result<()> {

let pin = PinnedIdentity {
did: cmd.did.clone(),
public_key_hex,
public_key_hex: public_key_hex.clone(),
curve: auths_crypto::CurveType::from_public_key_len(
hex::decode(public_key_hex.as_str())
.map(|b| b.len())
.unwrap_or(0),
)
.unwrap_or(auths_crypto::CurveType::Ed25519),
kel_tip_said: cmd.kel_tip,
kel_sequence: None,
first_seen: now,
Expand Down
2 changes: 2 additions & 0 deletions crates/auths-cli/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// crate-level allow during curve-agnostic refactor.
#![allow(clippy::disallowed_methods)]
// CLI is the presentation boundary — printing and exit are expected here.
#![allow(clippy::print_stdout, clippy::print_stderr, clippy::exit)]
pub mod adapters;
Expand Down
5 changes: 3 additions & 2 deletions crates/auths-cli/tests/cases/verify.rs
Original file line number Diff line number Diff line change
Expand Up @@ -197,11 +197,12 @@ fn test_verify_with_roots_json_explicit_policy() {
let roots_path = roots_dir.path().join("roots.json");
let roots_content = format!(
r#"{{
"version": 1,
"version": 2,
"roots": [
{{
"did": "{}",
"public_key_hex": "{}",
"curve": "ed25519",
"note": "Test issuer"
}}
]
Expand Down Expand Up @@ -234,7 +235,7 @@ fn test_verify_explicit_rejects_unknown_identity() {
// Create an empty roots.json file (no matching identity)
let roots_dir = tempfile::tempdir().unwrap();
let roots_path = roots_dir.path().join("roots.json");
std::fs::write(&roots_path, r#"{"version": 1, "roots": []}"#).unwrap();
std::fs::write(&roots_path, r#"{"version": 2, "roots": []}"#).unwrap();

// --trust and --roots-file are on the device verify command
let mut cmd = Command::cargo_bin("auths").unwrap();
Expand Down
3 changes: 3 additions & 0 deletions crates/auths-core/benches/crypto.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
// allow during curve-agnostic refactor
#![allow(clippy::disallowed_methods)]

//! Benchmarks for cryptographic operations in auths-core.
//!
//! Run with: cargo bench --package auths_core
Expand Down
13 changes: 13 additions & 0 deletions crates/auths-core/clippy.toml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,19 @@ disallowed-methods = [
# === Sans-IO: network ===
{ path = "reqwest::Client::new", reason = "sans-IO crate — use a port trait for HTTP", allow-invalid = true },
{ path = "reqwest::get", reason = "sans-IO crate — use a port trait for HTTP", allow-invalid = true },

# === Curve-agnostic refactor (fn-114) — ban Ed25519-hardcoded APIs ===
# Removed in fn-114.40 cleanup after all sweeps complete.
{ path = "ring::signature::Ed25519KeyPair::from_pkcs8", reason = "use TypedSignerKey::from_pkcs8 — dispatches on curve.", allow-invalid = true },
{ path = "ring::signature::Ed25519KeyPair::from_seed_unchecked", reason = "use auths_crypto::sign(&TypedSeed, msg).", allow-invalid = true },
{ path = "ring::signature::Ed25519KeyPair::generate_pkcs8", reason = "use inception::generate_keypair_for_init(curve).", allow-invalid = true },
{ path = "ring::signature::UnparsedPublicKey::new", reason = "use DevicePublicKey::verify — dispatches on curve.", allow-invalid = true },
{ path = "auths_crypto::parse_ed25519_seed", reason = "use parse_key_material — returns TypedSeed.", allow-invalid = true },
{ path = "auths_crypto::parse_ed25519_key_material", reason = "use parse_key_material — returns TypedSeed.", allow-invalid = true },
{ path = "auths_core::crypto::provider_bridge::sign_ed25519_sync", reason = "use auths_crypto::sign(&TypedSeed, msg).", allow-invalid = true },
{ path = "auths_core::crypto::provider_bridge::ed25519_public_key_from_seed_sync", reason = "use auths_crypto::public_key(&TypedSeed) or TypedSignerKey::public_key().", allow-invalid = true },
{ path = "auths_crypto::did_key_to_ed25519", reason = "use did_key_decode — returns DecodedDidKey.", allow-invalid = true },
{ path = "auths_id::identity::resolve::ed25519_to_did_key", reason = "use did_key_decode + DecodedDidKey::P256/Ed25519.", allow-invalid = true },
]

disallowed-types = [
Expand Down
2 changes: 2 additions & 0 deletions crates/auths-core/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// crate-level allow during curve-agnostic refactor.
#![allow(clippy::disallowed_methods)]
#![warn(clippy::too_many_lines, clippy::cognitive_complexity)]
#![warn(missing_docs)]
//! # auths-core
Expand Down
14 changes: 8 additions & 6 deletions crates/auths-core/src/signing.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
//! Signing abstractions and DID resolution.

use crate::crypto::provider_bridge;
use crate::crypto::signer::{decrypt_keypair, extract_seed_from_key_bytes};
use crate::crypto::signer::decrypt_keypair;
use crate::error::AgentError;
use crate::storage::keychain::{IdentityDID, KeyAlias, KeyStorage};

Expand Down Expand Up @@ -304,10 +303,13 @@ impl<S: KeyStorage + Send + Sync + 'static> SecureSigner for StorageSigner<S> {
}
};

let seed = extract_seed_from_key_bytes(&key_bytes)?;

provider_bridge::sign_ed25519_sync(&seed, message)
.map_err(|e| AgentError::CryptoError(format!("Ed25519 signing failed: {}", e)))
// fn-114.23: parse curve-tagged seed and dispatch sign on curve.
// Previously hardcoded sign_ed25519_sync which silently produced garbage
// signatures for P-256 identities.
let parsed = auths_crypto::parse_key_material(&key_bytes)
.map_err(|e| AgentError::KeyDeserializationError(e.to_string()))?;
auths_crypto::typed_sign(&parsed.seed, message)
.map_err(|e| AgentError::CryptoError(format!("signing failed: {}", e)))
}

fn sign_for_identity(
Expand Down
6 changes: 6 additions & 0 deletions crates/auths-core/src/trust/pinned.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ pub struct PinnedIdentity {
/// Root public key, raw bytes stored as lowercase hex.
pub public_key_hex: PublicKeyHex,

/// Curve of the pinned key (fn-114.34). Required — pre-launch hard break, no
/// v1 fallback. Old pin files without this field fail to deserialize; users
/// re-pin.
pub curve: auths_crypto::CurveType,

/// KEL tip SAID at the time of pinning (enables rotation continuity check).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub kel_tip_said: Option<String>,
Expand Down Expand Up @@ -279,6 +284,7 @@ mod tests {
public_key_hex: PublicKeyHex::new_unchecked(
"0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20",
),
curve: auths_crypto::CurveType::Ed25519,
kel_tip_said: Some("ETip".to_string()),
kel_sequence: Some(0),
first_seen: Utc::now(),
Expand Down
Loading
Loading