From b4d44c863f7e1264d0e7fc5742087cb045d79d9b Mon Sep 17 00:00:00 2001 From: bordumb Date: Wed, 11 Mar 2026 18:11:11 +0000 Subject: [PATCH 1/9] feat(auths-verifier): add CommitOid, PublicKeyHex, PolicyId newtypes; move auths-index to batch 3 (fn-63.1, fn-63.7) --- Cargo.lock | 1 + crates/auths-index/Cargo.toml | 1 + crates/auths-verifier/src/core.rs | 281 ++++++++++++++++++ crates/auths-verifier/src/lib.rs | 7 +- crates/auths-verifier/tests/cases/mod.rs | 1 + crates/auths-verifier/tests/cases/newtypes.rs | 188 ++++++++++++ scripts/releases/2_crates.py | 10 +- 7 files changed, 481 insertions(+), 8 deletions(-) create mode 100644 crates/auths-verifier/tests/cases/newtypes.rs diff --git a/Cargo.lock b/Cargo.lock index e9659f26..92c83750 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -490,6 +490,7 @@ name = "auths-index" version = "0.0.1-rc.8" dependencies = [ "anyhow", + "auths-verifier", "chrono", "git2", "log", diff --git a/crates/auths-index/Cargo.toml b/crates/auths-index/Cargo.toml index ca23b406..0a582ddf 100644 --- a/crates/auths-index/Cargo.toml +++ b/crates/auths-index/Cargo.toml @@ -12,6 +12,7 @@ publish = true license.workspace = true [dependencies] +auths-verifier.workspace = true sqlite = { version = "0.32", features = ["bundled"] } chrono = { version = "0.4", features = ["serde"] } anyhow = "1" diff --git a/crates/auths-verifier/src/core.rs b/crates/auths-verifier/src/core.rs index ebf6c024..9d6be1c5 100644 --- a/crates/auths-verifier/src/core.rs +++ b/crates/auths-verifier/src/core.rs @@ -1034,6 +1034,287 @@ impl ThresholdPolicy { } } +// ============================================================================= +// CommitOid newtype (validated) +// ============================================================================= + +/// Error type for `CommitOid` construction. +#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] +pub enum CommitOidError { + /// The string is empty. + #[error("commit OID is empty")] + Empty, + /// The string length is not 40 (SHA-1) or 64 (SHA-256). + #[error("expected 40 or 64 hex chars, got {0}")] + InvalidLength(usize), + /// The string contains non-hex characters. + #[error("invalid hex character in commit OID")] + InvalidHex, +} + +/// A validated Git commit object identifier (SHA-1 or SHA-256 hex string). +/// +/// Accepts exactly 40 lowercase hex characters (SHA-1) or 64 (SHA-256). +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)] +#[repr(transparent)] +#[serde(try_from = "String")] +pub struct CommitOid(String); + +impl CommitOid { + /// Parses and validates a commit OID string. + /// + /// Args: + /// * `raw`: A hex string that must be exactly 40 or 64 lowercase hex characters. + /// + /// Usage: + /// ```ignore + /// let oid = CommitOid::parse("a".repeat(40))?; + /// ``` + pub fn parse(raw: &str) -> Result { + let s = raw.trim().to_lowercase(); + if s.is_empty() { + return Err(CommitOidError::Empty); + } + if s.len() != 40 && s.len() != 64 { + return Err(CommitOidError::InvalidLength(s.len())); + } + if !s.chars().all(|c| c.is_ascii_hexdigit()) { + return Err(CommitOidError::InvalidHex); + } + Ok(Self(s)) + } + + /// Creates a `CommitOid` without validation. + /// + /// Only use at deserialization boundaries where the value was previously validated. + pub fn new_unchecked(s: impl Into) -> Self { + Self(s.into()) + } + + /// Returns the inner string slice. + pub fn as_str(&self) -> &str { + &self.0 + } + + /// Consumes self and returns the inner `String`. + pub fn into_inner(self) -> String { + self.0 + } +} + +impl fmt::Display for CommitOid { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(&self.0) + } +} + +impl AsRef for CommitOid { + fn as_ref(&self) -> &str { + &self.0 + } +} + +impl TryFrom for CommitOid { + type Error = CommitOidError; + fn try_from(s: String) -> Result { + Self::parse(&s) + } +} + +impl TryFrom<&str> for CommitOid { + type Error = CommitOidError; + fn try_from(s: &str) -> Result { + Self::parse(s) + } +} + +impl FromStr for CommitOid { + type Err = CommitOidError; + fn from_str(s: &str) -> Result { + Self::parse(s) + } +} + +impl From for String { + fn from(oid: CommitOid) -> Self { + oid.0 + } +} + +impl<'de> Deserialize<'de> for CommitOid { + fn deserialize>(d: D) -> Result { + let s = String::deserialize(d)?; + Self::parse(&s).map_err(serde::de::Error::custom) + } +} + +// ============================================================================= +// PublicKeyHex newtype (validated) +// ============================================================================= + +/// Error type for `PublicKeyHex` construction. +#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] +pub enum PublicKeyHexError { + /// The hex string has the wrong length (not 64 chars / 32 bytes). + #[error("expected 64 hex chars (32 bytes), got {0} chars")] + InvalidLength(usize), + /// The string contains non-hex characters. + #[error("invalid hex: {0}")] + InvalidHex(String), +} + +/// A validated hex-encoded Ed25519 public key (64 hex chars = 32 bytes). +/// +/// Use `to_ed25519()` to convert to the byte-array `Ed25519PublicKey` type. +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)] +#[repr(transparent)] +#[serde(try_from = "String")] +pub struct PublicKeyHex(String); + +impl PublicKeyHex { + /// Parses and validates a hex-encoded public key string. + /// + /// Args: + /// * `raw`: A 64-character hex string encoding 32 bytes. + /// + /// Usage: + /// ```ignore + /// let pk = PublicKeyHex::parse("ab".repeat(32))?; + /// ``` + pub fn parse(raw: &str) -> Result { + let s = raw.trim().to_lowercase(); + let bytes = hex::decode(&s).map_err(|e| PublicKeyHexError::InvalidHex(e.to_string()))?; + if bytes.len() != 32 { + return Err(PublicKeyHexError::InvalidLength(s.len())); + } + Ok(Self(s)) + } + + /// Creates a `PublicKeyHex` without validation. + /// + /// Only use at deserialization boundaries where the value was previously validated. + pub fn new_unchecked(s: impl Into) -> Self { + Self(s.into()) + } + + /// Returns the inner string slice. + pub fn as_str(&self) -> &str { + &self.0 + } + + /// Consumes self and returns the inner `String`. + pub fn into_inner(self) -> String { + self.0 + } + + /// Decodes the hex and returns the byte-array `Ed25519PublicKey`. + /// + /// Usage: + /// ```ignore + /// let pk_hex = PublicKeyHex::parse("ab".repeat(32))?; + /// let pk = pk_hex.to_ed25519()?; + /// ``` + pub fn to_ed25519(&self) -> Result { + let bytes = hex::decode(&self.0).map_err(|e| Ed25519KeyError::InvalidHex(e.to_string()))?; + Ed25519PublicKey::try_from_slice(&bytes) + } +} + +impl fmt::Display for PublicKeyHex { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(&self.0) + } +} + +impl AsRef for PublicKeyHex { + fn as_ref(&self) -> &str { + &self.0 + } +} + +impl TryFrom for PublicKeyHex { + type Error = PublicKeyHexError; + fn try_from(s: String) -> Result { + Self::parse(&s) + } +} + +impl TryFrom<&str> for PublicKeyHex { + type Error = PublicKeyHexError; + fn try_from(s: &str) -> Result { + Self::parse(s) + } +} + +impl FromStr for PublicKeyHex { + type Err = PublicKeyHexError; + fn from_str(s: &str) -> Result { + Self::parse(s) + } +} + +impl From for String { + fn from(pk: PublicKeyHex) -> Self { + pk.0 + } +} + +impl<'de> Deserialize<'de> for PublicKeyHex { + fn deserialize>(d: D) -> Result { + let s = String::deserialize(d)?; + Self::parse(&s).map_err(serde::de::Error::custom) + } +} + +// ============================================================================= +// PolicyId newtype (unvalidated) +// ============================================================================= + +/// An opaque policy identifier. +/// +/// No validation — wraps any `String`. Use where policy IDs are passed around +/// without needing to inspect their content. +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[serde(transparent)] +pub struct PolicyId(String); + +impl PolicyId { + /// Creates a new PolicyId. + pub fn new(s: impl Into) -> Self { + Self(s.into()) + } + + /// Returns the inner string slice. + pub fn as_str(&self) -> &str { + &self.0 + } +} + +impl Deref for PolicyId { + type Target = str; + fn deref(&self) -> &str { + &self.0 + } +} + +impl fmt::Display for PolicyId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(&self.0) + } +} + +impl From for PolicyId { + fn from(s: String) -> Self { + Self(s) + } +} + +impl From<&str> for PolicyId { + fn from(s: &str) -> Self { + Self(s.to_string()) + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/auths-verifier/src/lib.rs b/crates/auths-verifier/src/lib.rs index 023a525b..b47696ae 100644 --- a/crates/auths-verifier/src/lib.rs +++ b/crates/auths-verifier/src/lib.rs @@ -78,9 +78,10 @@ pub use action::ActionEnvelope; // Re-export core types pub use core::{ - Capability, CapabilityError, Ed25519KeyError, Ed25519PublicKey, Ed25519Signature, - IdentityBundle, MAX_ATTESTATION_JSON_SIZE, MAX_JSON_BATCH_SIZE, ResourceId, Role, - RoleParseError, SignatureLengthError, ThresholdPolicy, VerifiedAttestation, + Capability, CapabilityError, CommitOid, CommitOidError, Ed25519KeyError, Ed25519PublicKey, + Ed25519Signature, IdentityBundle, MAX_ATTESTATION_JSON_SIZE, MAX_JSON_BATCH_SIZE, PolicyId, + PublicKeyHex, PublicKeyHexError, ResourceId, Role, RoleParseError, SignatureLengthError, + ThresholdPolicy, VerifiedAttestation, }; // Re-export error types diff --git a/crates/auths-verifier/tests/cases/mod.rs b/crates/auths-verifier/tests/cases/mod.rs index f6e7e31d..6cfa1bd1 100644 --- a/crates/auths-verifier/tests/cases/mod.rs +++ b/crates/auths-verifier/tests/cases/mod.rs @@ -5,6 +5,7 @@ mod expiration_skew; #[cfg(feature = "ffi")] mod ffi_smoke; mod kel_verification; +mod newtypes; mod proptest_core; mod revocation_adversarial; mod serialization_pinning; diff --git a/crates/auths-verifier/tests/cases/newtypes.rs b/crates/auths-verifier/tests/cases/newtypes.rs new file mode 100644 index 00000000..b922a308 --- /dev/null +++ b/crates/auths-verifier/tests/cases/newtypes.rs @@ -0,0 +1,188 @@ +use auths_verifier::{CommitOid, CommitOidError, PolicyId, PublicKeyHex, PublicKeyHexError}; + +// ============================================================================= +// CommitOid tests +// ============================================================================= + +#[test] +fn commit_oid_valid_sha1() { + let hex40 = "a".repeat(40); + let oid = CommitOid::parse(&hex40).unwrap(); + assert_eq!(oid.as_str(), hex40); +} + +#[test] +fn commit_oid_valid_sha256() { + let hex64 = "b".repeat(64); + let oid = CommitOid::parse(&hex64).unwrap(); + assert_eq!(oid.as_str(), hex64); +} + +#[test] +fn commit_oid_rejects_invalid_hex() { + assert!(matches!( + CommitOid::parse(&"g".repeat(40)), + Err(CommitOidError::InvalidHex) + )); +} + +#[test] +fn commit_oid_rejects_wrong_length() { + assert!(matches!( + CommitOid::parse(&"a".repeat(20)), + Err(CommitOidError::InvalidLength(20)) + )); +} + +#[test] +fn commit_oid_rejects_empty() { + assert!(matches!(CommitOid::parse(""), Err(CommitOidError::Empty))); +} + +#[test] +fn commit_oid_display_fromstr_roundtrip() { + let hex40 = "c".repeat(40); + let oid = CommitOid::parse(&hex40).unwrap(); + let parsed: CommitOid = oid.to_string().parse().unwrap(); + assert_eq!(oid, parsed); +} + +#[test] +fn commit_oid_try_from_string() { + let hex40 = "d".repeat(40); + let oid: CommitOid = hex40.clone().try_into().unwrap(); + assert_eq!(oid.as_str(), hex40); +} + +#[test] +fn commit_oid_try_from_str() { + let hex40 = "e".repeat(40); + let oid = CommitOid::try_from(hex40.as_str()).unwrap(); + assert_eq!(oid.as_str(), hex40); +} + +#[test] +fn commit_oid_serde_roundtrip() { + let hex40 = "f".repeat(40); + let oid = CommitOid::parse(&hex40).unwrap(); + let json = serde_json::to_string(&oid).unwrap(); + let back: CommitOid = serde_json::from_str(&json).unwrap(); + assert_eq!(oid, back); +} + +#[test] +fn commit_oid_serde_rejects_invalid() { + let json = r#""not-a-valid-oid""#; + assert!(serde_json::from_str::(json).is_err()); +} + +#[test] +fn commit_oid_into_string() { + let hex40 = "a".repeat(40); + let oid = CommitOid::parse(&hex40).unwrap(); + let s: String = oid.into(); + assert_eq!(s, hex40); +} + +#[test] +fn commit_oid_normalizes_to_lowercase() { + let upper = "A".repeat(40); + let oid = CommitOid::parse(&upper).unwrap(); + assert_eq!(oid.as_str(), "a".repeat(40)); +} + +// ============================================================================= +// PublicKeyHex tests +// ============================================================================= + +#[test] +fn public_key_hex_valid() { + let hex64 = "ab".repeat(32); + let pk = PublicKeyHex::parse(&hex64).unwrap(); + assert_eq!(pk.as_str(), hex64); +} + +#[test] +fn public_key_hex_to_ed25519() { + let hex64 = "ab".repeat(32); + let pk = PublicKeyHex::parse(&hex64).unwrap(); + let ed = pk.to_ed25519().unwrap(); + assert_eq!(ed.as_bytes()[0], 0xab); +} + +#[test] +fn public_key_hex_rejects_invalid_hex() { + assert!(matches!( + PublicKeyHex::parse("zz".repeat(32).as_str()), + Err(PublicKeyHexError::InvalidHex(_)) + )); +} + +#[test] +fn public_key_hex_rejects_wrong_length() { + assert!(matches!( + PublicKeyHex::parse(&"ab".repeat(16)), + Err(PublicKeyHexError::InvalidLength(32)) + )); +} + +#[test] +fn public_key_hex_serde_roundtrip() { + let hex64 = "cd".repeat(32); + let pk = PublicKeyHex::parse(&hex64).unwrap(); + let json = serde_json::to_string(&pk).unwrap(); + let back: PublicKeyHex = serde_json::from_str(&json).unwrap(); + assert_eq!(pk, back); +} + +#[test] +fn public_key_hex_serde_rejects_invalid() { + let json = r#""too-short""#; + assert!(serde_json::from_str::(json).is_err()); +} + +#[test] +fn public_key_hex_into_string() { + let hex64 = "ef".repeat(32); + let pk = PublicKeyHex::parse(&hex64).unwrap(); + let s: String = pk.into(); + assert_eq!(s, hex64); +} + +// ============================================================================= +// PolicyId tests +// ============================================================================= + +#[test] +fn policy_id_construction() { + let pid = PolicyId::new("my-policy"); + assert_eq!(pid.as_str(), "my-policy"); + assert_eq!(&*pid, "my-policy"); // Deref +} + +#[test] +fn policy_id_from_string() { + let pid: PolicyId = "test-policy".into(); + assert_eq!(pid.as_str(), "test-policy"); +} + +#[test] +fn policy_id_from_owned_string() { + let pid = PolicyId::from(String::from("owned")); + assert_eq!(pid.as_str(), "owned"); +} + +#[test] +fn policy_id_display() { + let pid = PolicyId::new("display-test"); + assert_eq!(format!("{pid}"), "display-test"); +} + +#[test] +fn policy_id_serde_transparent_roundtrip() { + let pid = PolicyId::new("serde-test"); + let json = serde_json::to_string(&pid).unwrap(); + assert_eq!(json, r#""serde-test""#); + let back: PolicyId = serde_json::from_str(&json).unwrap(); + assert_eq!(pid, back); +} diff --git a/scripts/releases/2_crates.py b/scripts/releases/2_crates.py index 2ce7f6ff..1fb053a4 100644 --- a/scripts/releases/2_crates.py +++ b/scripts/releases/2_crates.py @@ -20,9 +20,9 @@ - git tag v{version} must exist (run github.py --push first) Publish order (dependency layers): - Batch 1: auths, auths-crypto, auths-index, auths-jwt, auths-policy, auths-telemetry - Batch 2: auths-verifier, auths-keri - Batch 3: auths-core + Batch 1: auths, auths-crypto, auths-jwt, auths-policy, auths-telemetry + Batch 2: auths-verifier, auths-keri, auths-pairing-protocol + Batch 3: auths-core, auths-index Batch 4: auths-infra-http, auths-mcp-server Batch 5: auths-id (depends on core, crypto, policy, verifier, infra-http) Batch 6: auths-storage, auths-sdk, auths-radicle, auths-pairing-daemon (depend on auths-id/core) @@ -42,9 +42,9 @@ CRATES_IO_API = "https://crates.io/api/v1/crates" PUBLISH_BATCHES: list[list[str]] = [ - ["auths", "auths-crypto", "auths-index", "auths-jwt", "auths-policy", "auths-telemetry"], + ["auths", "auths-crypto", "auths-jwt", "auths-policy", "auths-telemetry"], ["auths-verifier", "auths-keri", "auths-pairing-protocol"], - ["auths-core"], + ["auths-core", "auths-index"], ["auths-infra-http", "auths-mcp-server"], ["auths-id"], ["auths-storage", "auths-sdk", "auths-radicle", "auths-pairing-daemon"], From 0bb28d5b1447d67c791753564dc6c2e10a0e7674 Mon Sep 17 00:00:00 2001 From: bordumb Date: Wed, 11 Mar 2026 18:59:30 +0000 Subject: [PATCH 2/9] refactor: thread ResourceId/Prefix/Said/PublicKeyHex newtypes across workspace (fn-63.2, fn-63.4) --- .../src/commands/artifact/publish.rs | 3 +- .../auths-cli/src/commands/artifact/verify.rs | 3 +- crates/auths-cli/src/commands/id/identity.rs | 4 +- crates/auths-cli/src/commands/org.rs | 10 ++-- crates/auths-cli/src/commands/trust.rs | 16 ++---- .../auths-cli/src/commands/verify_commit.rs | 14 ++--- crates/auths-core/src/trust/pinned.rs | 53 +++++++++---------- crates/auths-core/src/trust/resolve.rs | 13 +++-- crates/auths-core/src/trust/roots_file.rs | 28 +++------- crates/auths-id/src/identity/helpers.rs | 4 +- crates/auths-id/src/storage/indexed.rs | 2 +- crates/auths-index/src/index.rs | 52 +++++++++--------- crates/auths-index/src/rebuild.rs | 3 +- crates/auths-sdk/src/signing.rs | 6 +-- crates/auths-sdk/src/workflows/artifact.rs | 3 +- crates/auths-sdk/src/workflows/mcp.rs | 7 +-- crates/auths-sdk/src/workflows/org.rs | 13 ++--- crates/auths-sdk/tests/cases/artifact.rs | 4 +- crates/auths-sdk/tests/cases/org.rs | 7 +-- crates/auths-storage/src/git/adapter.rs | 24 ++++----- .../src/git/attestation_adapter.rs | 2 +- crates/auths-verifier/src/core.rs | 16 ++++-- 22 files changed, 140 insertions(+), 147 deletions(-) diff --git a/crates/auths-cli/src/commands/artifact/publish.rs b/crates/auths-cli/src/commands/artifact/publish.rs index cdb63f67..4dc571f4 100644 --- a/crates/auths-cli/src/commands/artifact/publish.rs +++ b/crates/auths-cli/src/commands/artifact/publish.rs @@ -6,13 +6,14 @@ use auths_infra_http::HttpRegistryClient; use auths_sdk::workflows::artifact::{ ArtifactPublishConfig, ArtifactPublishError, publish_artifact, }; +use auths_verifier::core::ResourceId; use serde::Serialize; use crate::ux::format::{JsonResponse, Output, is_json_mode}; #[derive(Serialize)] struct PublishJsonResponse { - attestation_rid: String, + attestation_rid: ResourceId, registry: String, package_name: Option, signer_did: String, diff --git a/crates/auths-cli/src/commands/artifact/verify.rs b/crates/auths-cli/src/commands/artifact/verify.rs index 54dc91d3..d428624b 100644 --- a/crates/auths-cli/src/commands/artifact/verify.rs +++ b/crates/auths-cli/src/commands/artifact/verify.rs @@ -215,7 +215,8 @@ fn resolve_identity_key( .with_context(|| format!("Failed to read identity bundle: {:?}", bundle_path))?; let bundle: IdentityBundle = serde_json::from_str(&bundle_content) .with_context(|| format!("Failed to parse identity bundle: {:?}", bundle_path))?; - let pk = hex::decode(&bundle.public_key_hex).context("Invalid public key hex in bundle")?; + let pk = hex::decode(bundle.public_key_hex.as_str()) + .context("Invalid public key hex in bundle")?; Ok((pk, bundle.identity_did)) } else { // Resolve public key from the issuer DID diff --git a/crates/auths-cli/src/commands/id/identity.rs b/crates/auths-cli/src/commands/id/identity.rs index 45cd9f8c..ecd1f943 100644 --- a/crates/auths-cli/src/commands/id/identity.rs +++ b/crates/auths-cli/src/commands/id/identity.rs @@ -598,7 +598,9 @@ 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)?; - let public_key_hex = hex::encode(keypair.public_key().as_ref()); + let public_key_hex = auths_verifier::PublicKeyHex::new_unchecked(hex::encode( + keypair.public_key().as_ref(), + )); // Create the bundle let bundle = IdentityBundle { diff --git a/crates/auths-cli/src/commands/org.rs b/crates/auths-cli/src/commands/org.rs index cd78f65e..ae0b35f2 100644 --- a/crates/auths-cli/src/commands/org.rs +++ b/crates/auths-cli/src/commands/org.rs @@ -31,7 +31,7 @@ use auths_storage::git::{ GitRegistryBackend, RegistryAttestationStorage, RegistryConfig, RegistryIdentityStorage, }; use auths_verifier::types::DeviceDID; -use auths_verifier::{Capability, Ed25519PublicKey, Prefix}; +use auths_verifier::{Capability, Ed25519PublicKey, Prefix, PublicKeyHex}; use clap::ValueEnum; @@ -665,7 +665,7 @@ pub fn handle_org(cmd: OrgCommand, ctx: &crate::config::CliConfig) -> Result<()> let (stored_did, _role, _encrypted_key) = key_storage .load_key(&signer_alias) .with_context(|| format!("Failed to load signer key '{}'", signer_alias))?; - let admin_pk_hex = hex::encode( + let admin_pk_hex = PublicKeyHex::new_unchecked(hex::encode( resolver .resolve(stored_did.as_str()) .with_context(|| { @@ -673,7 +673,7 @@ pub fn handle_org(cmd: OrgCommand, ctx: &crate::config::CliConfig) -> Result<()> })? .public_key() .as_bytes(), - ); + )); let member_resolved = resolver .resolve(&member) @@ -779,7 +779,7 @@ pub fn handle_org(cmd: OrgCommand, ctx: &crate::config::CliConfig) -> Result<()> let (stored_did, _role, _encrypted_key) = key_storage .load_key(&signer_alias) .with_context(|| format!("Failed to load signer key '{}'", signer_alias))?; - let admin_pk_hex = hex::encode( + let admin_pk_hex = PublicKeyHex::new_unchecked(hex::encode( resolver .resolve(stored_did.as_str()) .with_context(|| { @@ -787,7 +787,7 @@ pub fn handle_org(cmd: OrgCommand, ctx: &crate::config::CliConfig) -> Result<()> })? .public_key() .as_bytes(), - ); + )); let member_resolved = resolver .resolve(&member) diff --git a/crates/auths-cli/src/commands/trust.rs b/crates/auths-cli/src/commands/trust.rs index 1a7efdd9..008aedbd 100644 --- a/crates/auths-cli/src/commands/trust.rs +++ b/crates/auths-cli/src/commands/trust.rs @@ -3,8 +3,9 @@ //! Manage pinned identity roots for trust-on-first-use (TOFU) and explicit trust. use crate::ux::format::{JsonResponse, Output, is_json_mode}; -use anyhow::{Result, anyhow}; +use anyhow::{Context, Result, anyhow}; use auths_core::trust::{PinnedIdentity, PinnedIdentityStore, TrustLevel}; +use auths_verifier::PublicKeyHex; use chrono::Utc; use clap::{Parser, Subcommand}; use serde::Serialize; @@ -96,7 +97,7 @@ struct PinSummary { #[derive(Debug, Serialize)] struct PinDetails { did: String, - public_key_hex: String, + public_key_hex: PublicKeyHex, trust_level: String, first_seen: String, origin: String, @@ -160,14 +161,7 @@ fn handle_list(_cmd: TrustListCommand) -> Result<()> { } fn handle_pin(cmd: TrustPinCommand) -> Result<()> { - // Validate hex format and length - let bytes = hex::decode(&cmd.key).map_err(|e| anyhow!("Invalid hex for public key: {}", e))?; - if bytes.len() != 32 { - anyhow::bail!( - "Invalid key length: expected 32 bytes (64 hex chars), got {} bytes", - bytes.len() - ); - } + let public_key_hex = PublicKeyHex::parse(&cmd.key).context("Invalid public key hex")?; let store = PinnedIdentityStore::new(PinnedIdentityStore::default_path()); @@ -183,7 +177,7 @@ fn handle_pin(cmd: TrustPinCommand) -> Result<()> { let pin = PinnedIdentity { did: cmd.did.clone(), - public_key_hex: cmd.key.clone(), + public_key_hex, kel_tip_said: cmd.kel_tip, kel_sequence: None, first_seen: Utc::now(), diff --git a/crates/auths-cli/src/commands/verify_commit.rs b/crates/auths-cli/src/commands/verify_commit.rs index d9d5f54a..5e21544e 100644 --- a/crates/auths-cli/src/commands/verify_commit.rs +++ b/crates/auths-cli/src/commands/verify_commit.rs @@ -140,8 +140,8 @@ fn resolve_signers_source(cmd: &VerifyCommitCommand) -> Result { let bundle: IdentityBundle = serde_json::from_str(&bundle_content) .with_context(|| format!("Failed to parse identity bundle: {:?}", bundle_path))?; - let public_key_bytes = - hex::decode(&bundle.public_key_hex).context("Invalid public key hex in bundle")?; + let public_key_bytes = hex::decode(bundle.public_key_hex.as_str()) + .context("Invalid public key hex in bundle")?; let ssh_key = format_ed25519_as_ssh(&public_key_bytes)?; let temp_signers_content = format!("{} {}", bundle.identity_did, ssh_key); @@ -348,7 +348,7 @@ async fn verify_bundle_chain( ); } - let root_pk = match hex::decode(&bundle.public_key_hex) { + let root_pk = match hex::decode(bundle.public_key_hex.as_str()) { Ok(pk) => pk, Err(e) => { return ( @@ -418,8 +418,8 @@ async fn verify_witnesses( if let Some(bundle) = bundle && !bundle.attestation_chain.is_empty() { - let root_pk = - hex::decode(&bundle.public_key_hex).context("Invalid public key hex in bundle")?; + let root_pk = hex::decode(bundle.public_key_hex.as_str()) + .context("Invalid public key hex in bundle")?; let report = verify_chain_with_witnesses(&bundle.attestation_chain, &root_pk, &config) .await @@ -914,7 +914,7 @@ mod tests { async fn verify_bundle_chain_empty_chain() { let bundle = IdentityBundle { identity_did: auths_verifier::IdentityDID::new_unchecked("did:keri:test"), - public_key_hex: "aa".repeat(32), + public_key_hex: auths_verifier::PublicKeyHex::new_unchecked("aa".repeat(32)), attestation_chain: vec![], bundle_timestamp: Utc::now(), max_valid_for_secs: 86400, @@ -930,7 +930,7 @@ mod tests { async fn verify_bundle_chain_invalid_hex() { let bundle = IdentityBundle { identity_did: auths_verifier::IdentityDID::new_unchecked("did:keri:test"), - public_key_hex: "not_hex".into(), + public_key_hex: auths_verifier::PublicKeyHex::new_unchecked("not_hex"), attestation_chain: vec![auths_verifier::core::Attestation { version: 1, rid: "test".into(), diff --git a/crates/auths-core/src/trust/pinned.rs b/crates/auths-core/src/trust/pinned.rs index 683e1e58..5067b511 100644 --- a/crates/auths-core/src/trust/pinned.rs +++ b/crates/auths-core/src/trust/pinned.rs @@ -7,6 +7,7 @@ use std::fs; use std::io::Write; use std::path::PathBuf; +use auths_verifier::PublicKeyHex; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; @@ -22,10 +23,7 @@ pub struct PinnedIdentity { pub did: String, /// Root public key, raw bytes stored as lowercase hex. - /// - /// Always normalized at pin-time via `hex::encode`. - /// All comparisons happen on decoded bytes, never on strings. - pub public_key_hex: String, + pub public_key_hex: PublicKeyHex, /// KEL tip SAID at the time of pinning (enables rotation continuity check). #[serde(default, skip_serializing_if = "Option::is_none")] @@ -50,7 +48,7 @@ impl PinnedIdentity { /// /// Validates hex at construction; this should never fail on a well-formed pin. pub fn public_key_bytes(&self) -> Result, TrustError> { - hex::decode(&self.public_key_hex).map_err(|e| { + hex::decode(self.public_key_hex.as_str()).map_err(|e| { TrustError::InvalidData(format!("Corrupt pin for {}: invalid hex: {}", self.did, e)) }) } @@ -128,9 +126,6 @@ impl PinnedIdentityStore { /// The public key hex is validated at pin-time. /// Errors if the DID is already pinned (use `update` for rotation). pub fn pin(&self, identity: PinnedIdentity) -> Result<(), TrustError> { - let _ = hex::decode(&identity.public_key_hex) - .map_err(|e| TrustError::InvalidData(format!("Invalid public_key_hex: {}", e)))?; - let _lock = self.lock()?; let mut entries = self.read_all()?; if entries.iter().any(|e| e.did == identity.did) { @@ -281,8 +276,9 @@ mod tests { fn make_test_pin() -> PinnedIdentity { PinnedIdentity { did: "did:keri:ETest123".to_string(), - public_key_hex: "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20" - .to_string(), + public_key_hex: PublicKeyHex::new_unchecked( + "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20", + ), kel_tip_said: Some("ETip".to_string()), kel_sequence: Some(0), first_seen: Utc::now(), @@ -301,12 +297,16 @@ mod tests { } #[test] - fn test_public_key_bytes_invalid_hex() { - let mut pin = make_test_pin(); - pin.public_key_hex = "not-valid-hex".to_string(); - let result = pin.public_key_bytes(); + fn test_serde_rejects_invalid_hex() { + let json = r#"{ + "did": "did:keri:ETest123", + "public_key_hex": "not-valid-hex", + "first_seen": "2024-01-01T00:00:00Z", + "origin": "test", + "trust_level": "tofu" + }"#; + let result: Result = serde_json::from_str(json); assert!(result.is_err()); - assert!(result.unwrap_err().to_string().contains("Corrupt pin")); } #[test] @@ -325,10 +325,10 @@ mod tests { #[test] fn test_key_matches_case_insensitive() { - // Mixed case hex should still match let mut pin = make_test_pin(); - pin.public_key_hex = - "0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F20".to_string(); + pin.public_key_hex = PublicKeyHex::new_unchecked( + "0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F20", + ); let expected: Vec = (1..=32).collect(); assert!(pin.key_matches(&expected).unwrap()); } @@ -404,14 +404,9 @@ mod tests { } #[test] - fn test_store_pin_rejects_invalid_hex() { - let (_dir, store) = temp_store(); - let mut pin = make_test_pin(); - pin.public_key_hex = "not-valid-hex".to_string(); - - let result = store.pin(pin); + fn test_public_key_hex_rejects_invalid_at_parse() { + let result = PublicKeyHex::parse("not-valid-hex"); assert!(result.is_err()); - assert!(result.unwrap_err().to_string().contains("Invalid")); } #[test] @@ -432,15 +427,15 @@ mod tests { let mut pin = make_test_pin(); store.pin(pin.clone()).unwrap(); - // Update with new key - pin.public_key_hex = - "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(); + pin.public_key_hex = PublicKeyHex::new_unchecked( + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + ); pin.kel_sequence = Some(1); store.update(pin.clone()).unwrap(); let found = store.lookup(&pin.did).unwrap().unwrap(); assert_eq!(found.kel_sequence, Some(1)); - assert!(found.public_key_hex.starts_with("aaaa")); + assert!(found.public_key_hex.as_str().starts_with("aaaa")); } #[test] diff --git a/crates/auths-core/src/trust/resolve.rs b/crates/auths-core/src/trust/resolve.rs index 181e2572..d9d27518 100644 --- a/crates/auths-core/src/trust/resolve.rs +++ b/crates/auths-core/src/trust/resolve.rs @@ -3,6 +3,8 @@ //! This module provides the core trust decision engine that determines //! whether to trust a presented identity based on the configured policy. +use auths_verifier::PublicKeyHex; + use super::continuity::{KelContinuityChecker, RotationProof}; use super::pinned::{PinnedIdentity, PinnedIdentityStore, TrustLevel}; use super::policy::TrustPolicy; @@ -155,7 +157,7 @@ pub fn resolve_trust( if prompt(&msg) { let pin = PinnedIdentity { did, - public_key_hex: pk_hex, + public_key_hex: PublicKeyHex::new_unchecked(pk_hex), kel_tip_said: None, kel_sequence: None, first_seen: now, @@ -186,7 +188,7 @@ pub fn resolve_trust( TrustDecision::RotationVerified { old_pin, proof } => { let updated = PinnedIdentity { did: old_pin.did, - public_key_hex: hex::encode(&proof.new_public_key), + 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), first_seen: old_pin.first_seen, @@ -198,7 +200,7 @@ pub fn resolve_trust( } TrustDecision::Conflict { pin, presented_pk } => { - let pinned_hex = &pin.public_key_hex; + let pinned_hex = pin.public_key_hex.as_str(); let presented_hex = hex::encode(&presented_pk); Err(TrustError::PolicyRejected(format!( "TRUST CONFLICT for {}\n \ @@ -225,8 +227,9 @@ mod tests { fn make_test_pin() -> PinnedIdentity { PinnedIdentity { did: "did:keri:ETest123".to_string(), - public_key_hex: "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20" - .to_string(), + public_key_hex: PublicKeyHex::new_unchecked( + "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20", + ), kel_tip_said: Some("ETip".to_string()), kel_sequence: Some(0), first_seen: Utc::now(), diff --git a/crates/auths-core/src/trust/roots_file.rs b/crates/auths-core/src/trust/roots_file.rs index ba6df6fd..5d5ffa6f 100644 --- a/crates/auths-core/src/trust/roots_file.rs +++ b/crates/auths-core/src/trust/roots_file.rs @@ -3,6 +3,7 @@ //! This module provides loading and validation for `.auths/roots.json` files, //! which allow repositories to define trusted identity roots for CI pipelines. +use auths_verifier::PublicKeyHex; use serde::Deserialize; use std::path::Path; @@ -44,7 +45,7 @@ pub struct RootEntry { pub did: String, /// The public key in hex format (64 chars, 32 bytes for Ed25519). - pub public_key_hex: String, + pub public_key_hex: PublicKeyHex, /// Optional KEL tip SAID for rotation-aware matching. #[serde(default)] @@ -69,22 +70,6 @@ impl RootsFile { ))); } - for root in &file.roots { - let bytes = hex::decode(&root.public_key_hex).map_err(|e| { - TrustError::InvalidData(format!( - "Invalid public_key_hex for {} in roots.json: {}", - root.did, e - )) - })?; - if bytes.len() != 32 { - return Err(TrustError::InvalidData(format!( - "Invalid key length for {} in roots.json: expected 32 bytes, got {}", - root.did, - bytes.len() - ))); - } - } - Ok(file) } @@ -109,7 +94,7 @@ impl RootsFile { impl RootEntry { /// Decode the public key to raw bytes. pub fn public_key_bytes(&self) -> Result, TrustError> { - hex::decode(&self.public_key_hex) + hex::decode(self.public_key_hex.as_str()) .map_err(|e| TrustError::InvalidData(format!("Invalid public_key_hex: {}", e))) } } @@ -202,7 +187,6 @@ mod tests { let result = RootsFile::load(&path); assert!(result.is_err()); - assert!(result.unwrap_err().to_string().contains("Invalid")); } #[test] @@ -221,7 +205,6 @@ mod tests { let result = RootsFile::load(&path); assert!(result.is_err()); - assert!(result.unwrap_err().to_string().contains("32 bytes")); } #[test] @@ -277,8 +260,9 @@ mod tests { fn test_root_entry_public_key_bytes() { let entry = RootEntry { did: "did:keri:ETest".to_string(), - public_key_hex: "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20" - .to_string(), + public_key_hex: PublicKeyHex::new_unchecked( + "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20", + ), kel_tip_said: None, note: None, }; diff --git a/crates/auths-id/src/identity/helpers.rs b/crates/auths-id/src/identity/helpers.rs index b050830e..ff32ccc9 100644 --- a/crates/auths-id/src/identity/helpers.rs +++ b/crates/auths-id/src/identity/helpers.rs @@ -14,7 +14,7 @@ use ring::signature::Ed25519KeyPair; use crate::storage::attestation::AttestationSource; -use auths_verifier::core::Attestation; +use auths_verifier::core::{Attestation, ResourceId}; use auths_verifier::types::DeviceDID; pub use crate::identity::managed::ManagedIdentity; @@ -25,7 +25,7 @@ const OID_ED25519: pkcs8::der::asn1::ObjectIdentifier = #[derive(Debug, Clone)] pub struct Identity { pub did: String, - pub rid: String, + pub rid: ResourceId, pub device_dids: Vec, } diff --git a/crates/auths-id/src/storage/indexed.rs b/crates/auths-id/src/storage/indexed.rs index d0f6d98e..5839b557 100644 --- a/crates/auths-id/src/storage/indexed.rs +++ b/crates/auths-id/src/storage/indexed.rs @@ -87,7 +87,7 @@ impl IndexedAttestationStorage { now: DateTime, ) -> Result<(), StorageError> { let indexed = IndexedAttestation { - rid: att.rid.to_string(), + rid: att.rid.clone(), issuer_did: att.issuer.to_string(), device_did: att.subject.to_string(), git_ref: git_ref.to_string(), diff --git a/crates/auths-index/src/index.rs b/crates/auths-index/src/index.rs index a23e0d9d..70eefef1 100644 --- a/crates/auths-index/src/index.rs +++ b/crates/auths-index/src/index.rs @@ -1,5 +1,7 @@ use crate::error::Result; use crate::schema; +use auths_verifier::core::ResourceId; +use auths_verifier::keri::{Prefix, Said}; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use sqlite::Connection; @@ -10,7 +12,7 @@ use std::path::Path; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct IndexedAttestation { /// Primary key - the attestation RID - pub rid: String, + pub rid: ResourceId, /// DID of the issuer (controller) pub issuer_did: String, /// DID of the device this attestation is for @@ -33,10 +35,10 @@ pub struct IndexedAttestation { /// Full `KeyState` is loaded from `GitRegistryBackend` when needed. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct IndexedIdentity { - pub prefix: String, + pub prefix: Prefix, pub current_keys: Vec, pub sequence: u64, - pub tip_said: String, + pub tip_said: Said, pub updated_at: DateTime, } @@ -46,10 +48,10 @@ pub struct IndexedIdentity { /// Full `Attestation` is loaded from Git when policy evaluation needs it. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct IndexedOrgMember { - pub org_prefix: String, + pub org_prefix: Prefix, pub member_did: String, pub issuer_did: String, - pub rid: String, + pub rid: ResourceId, pub revoked_at: Option>, pub expires_at: Option>, pub updated_at: DateTime, @@ -274,7 +276,7 @@ impl AttestationIndex { .unwrap_or_else(|_| Utc::now()); Ok(IndexedAttestation { - rid, + rid: ResourceId::new(rid), issuer_did, device_did, git_ref, @@ -335,10 +337,10 @@ impl AttestationIndex { .map(|dt| dt.with_timezone(&Utc)) .unwrap_or_else(|_| Utc::now()); Ok(Some(IndexedIdentity { - prefix, + prefix: Prefix::new_unchecked(prefix), current_keys, sequence: sequence as u64, - tip_said, + tip_said: Said::new_unchecked(tip_said), updated_at, })) } else { @@ -404,10 +406,10 @@ impl AttestationIndex { let updated_str: String = stmt.read(6)?; members.push(IndexedOrgMember { - org_prefix, + org_prefix: Prefix::new_unchecked(org_prefix), member_did, issuer_did, - rid, + rid: ResourceId::new(rid), revoked_at: parse_dt(revoked_str), expires_at: parse_dt(expires_str), updated_at: DateTime::parse_from_rfc3339(&updated_str) @@ -455,7 +457,7 @@ mod tests { revoked_at: Option>, ) -> IndexedAttestation { IndexedAttestation { - rid: rid.to_string(), + rid: ResourceId::new(rid), issuer_did: "did:key:issuer123".to_string(), device_did: device.to_string(), git_ref: format!("refs/auths/devices/nodes/{}/signatures", device), @@ -582,10 +584,10 @@ mod tests { fn test_upsert_and_query_identity() { let index = AttestationIndex::in_memory().unwrap(); let identity = IndexedIdentity { - prefix: "ETestPrefix123".to_string(), + prefix: Prefix::new_unchecked("ETestPrefix123".to_string()), current_keys: vec!["DKey1".to_string(), "DKey2".to_string()], sequence: 3, - tip_said: "ETipSaid123".to_string(), + tip_said: Said::new_unchecked("ETipSaid123".to_string()), updated_at: Utc::now(), }; @@ -604,19 +606,19 @@ mod tests { fn test_upsert_identity_updates_existing() { let index = AttestationIndex::in_memory().unwrap(); let identity = IndexedIdentity { - prefix: "ETestPrefix".to_string(), + prefix: Prefix::new_unchecked("ETestPrefix".to_string()), current_keys: vec!["DKey1".to_string()], sequence: 0, - tip_said: "ESaid0".to_string(), + tip_said: Said::new_unchecked("ESaid0".to_string()), updated_at: Utc::now(), }; index.upsert_identity(&identity).unwrap(); let updated = IndexedIdentity { - prefix: "ETestPrefix".to_string(), + prefix: Prefix::new_unchecked("ETestPrefix".to_string()), current_keys: vec!["DKey2".to_string()], sequence: 1, - tip_said: "ESaid1".to_string(), + tip_said: Said::new_unchecked("ESaid1".to_string()), updated_at: Utc::now(), }; index.upsert_identity(&updated).unwrap(); @@ -638,10 +640,10 @@ mod tests { fn test_upsert_and_list_org_members() { let index = AttestationIndex::in_memory().unwrap(); let member = IndexedOrgMember { - org_prefix: "did:keri:EOrg".to_string(), + org_prefix: Prefix::new_unchecked("did:keri:EOrg".to_string()), member_did: "did:key:z6MkMember1".to_string(), issuer_did: "did:keri:EOrg".to_string(), - rid: "rid-member-1".to_string(), + rid: ResourceId::new("rid-member-1"), revoked_at: None, expires_at: None, updated_at: Utc::now(), @@ -659,10 +661,10 @@ mod tests { fn test_upsert_org_member_updates_existing() { let index = AttestationIndex::in_memory().unwrap(); let member = IndexedOrgMember { - org_prefix: "did:keri:EOrg".to_string(), + org_prefix: Prefix::new_unchecked("did:keri:EOrg".to_string()), member_did: "did:key:z6MkMember1".to_string(), issuer_did: "did:keri:EOrg".to_string(), - rid: "rid-v1".to_string(), + rid: ResourceId::new("rid-v1"), revoked_at: None, expires_at: None, updated_at: Utc::now(), @@ -670,10 +672,10 @@ mod tests { index.upsert_org_member(&member).unwrap(); let updated = IndexedOrgMember { - org_prefix: "did:keri:EOrg".to_string(), + org_prefix: Prefix::new_unchecked("did:keri:EOrg".to_string()), member_did: "did:key:z6MkMember1".to_string(), issuer_did: "did:keri:EOrg".to_string(), - rid: "rid-v2".to_string(), + rid: ResourceId::new("rid-v2"), revoked_at: Some(Utc::now()), expires_at: None, updated_at: Utc::now(), @@ -694,10 +696,10 @@ mod tests { for i in 0..3 { index .upsert_org_member(&IndexedOrgMember { - org_prefix: "did:keri:EOrg".to_string(), + org_prefix: Prefix::new_unchecked("did:keri:EOrg".to_string()), member_did: format!("did:key:z6MkMember{}", i), issuer_did: "did:keri:EOrg".to_string(), - rid: format!("rid-{}", i), + rid: ResourceId::new(format!("rid-{}", i)), revoked_at: None, expires_at: None, updated_at: Utc::now(), diff --git a/crates/auths-index/src/rebuild.rs b/crates/auths-index/src/rebuild.rs index d842a330..40166719 100644 --- a/crates/auths-index/src/rebuild.rs +++ b/crates/auths-index/src/rebuild.rs @@ -1,5 +1,6 @@ use crate::error::Result; use crate::index::{AttestationIndex, IndexedAttestation}; +use auths_verifier::core::ResourceId; use chrono::Utc; use git2::Repository; use std::path::Path; @@ -117,7 +118,7 @@ fn extract_attestation_from_ref( .unwrap_or_else(Utc::now); Ok(IndexedAttestation { - rid, + rid: ResourceId::new(rid), issuer_did, device_did, git_ref: ref_name.to_string(), diff --git a/crates/auths-sdk/src/signing.rs b/crates/auths-sdk/src/signing.rs index b7cf8dcc..1ebd7b1e 100644 --- a/crates/auths-sdk/src/signing.rs +++ b/crates/auths-sdk/src/signing.rs @@ -12,7 +12,7 @@ use auths_core::storage::keychain::{IdentityDID, KeyAlias, KeyStorage}; use auths_id::attestation::core::resign_attestation; use auths_id::attestation::create::create_signed_attestation; use auths_id::storage::git_refs::AttestationMetadata; -use auths_verifier::core::Capability; +use auths_verifier::core::{Capability, ResourceId}; use auths_verifier::types::DeviceDID; use std::collections::HashMap; use std::path::Path; @@ -190,7 +190,7 @@ pub struct ArtifactSigningResult { /// Canonical JSON of the signed attestation. pub attestation_json: String, /// Resource identifier assigned to the attestation in the identity store. - pub rid: String, + pub rid: ResourceId, /// Hex-encoded SHA-256 digest of the attested artifact. pub digest: String, } @@ -391,7 +391,7 @@ pub fn sign_artifact( .metadata() .map_err(|e| ArtifactSigningError::DigestFailed(e.to_string()))?; - let rid = format!("sha256:{}", artifact_meta.digest.hex); + let rid = ResourceId::new(format!("sha256:{}", artifact_meta.digest.hex)); let now = ctx.clock.now(); let meta = AttestationMetadata { timestamp: Some(now), diff --git a/crates/auths-sdk/src/workflows/artifact.rs b/crates/auths-sdk/src/workflows/artifact.rs index 3f5a32ce..d76293c1 100644 --- a/crates/auths-sdk/src/workflows/artifact.rs +++ b/crates/auths-sdk/src/workflows/artifact.rs @@ -1,6 +1,7 @@ //! Artifact digest computation and publishing workflow. use auths_core::ports::network::{NetworkError, RegistryClient}; +use auths_verifier::core::ResourceId; use serde::Deserialize; use thiserror::Error; @@ -25,7 +26,7 @@ pub struct ArtifactPublishConfig { #[derive(Debug, Deserialize)] pub struct ArtifactPublishResult { /// Stable registry identifier for the stored attestation. - pub attestation_rid: String, + pub attestation_rid: ResourceId, /// Package identifier echoed back by the registry, if provided. pub package_name: Option, /// DID of the identity that signed the attestation. diff --git a/crates/auths-sdk/src/workflows/mcp.rs b/crates/auths-sdk/src/workflows/mcp.rs index 54b58536..2dac4d80 100644 --- a/crates/auths-sdk/src/workflows/mcp.rs +++ b/crates/auths-sdk/src/workflows/mcp.rs @@ -4,6 +4,7 @@ //! the OIDC bridge. The `reqwest::Client` is injected so callers can configure //! timeouts, certificate pinning, and test-time mocking. +use auths_verifier::PublicKeyHex; use auths_verifier::core::Attestation; use serde::{Deserialize, Serialize}; @@ -13,7 +14,7 @@ use crate::error::McpAuthError; #[derive(Serialize)] struct McpExchangeRequest { attestation_chain: Vec, - root_public_key: String, + root_public_key: PublicKeyHex, #[serde(skip_serializing_if = "Option::is_none")] requested_capabilities: Option>, } @@ -60,14 +61,14 @@ pub async fn exchange_token( client: &reqwest::Client, bridge_url: &str, chain: &[Attestation], - root_public_key_hex: &str, + root_public_key_hex: &PublicKeyHex, requested_capabilities: &[&str], ) -> Result { let url = format!("{}/token", bridge_url.trim_end_matches('/')); let request_body = McpExchangeRequest { attestation_chain: chain.to_vec(), - root_public_key: root_public_key_hex.to_string(), + root_public_key: root_public_key_hex.clone(), requested_capabilities: if requested_capabilities.is_empty() { None } else { diff --git a/crates/auths-sdk/src/workflows/org.rs b/crates/auths-sdk/src/workflows/org.rs index 2e937a2f..a1abcedc 100644 --- a/crates/auths-sdk/src/workflows/org.rs +++ b/crates/auths-sdk/src/workflows/org.rs @@ -15,6 +15,7 @@ use auths_id::attestation::revoke::create_signed_revocation; use auths_id::ports::registry::RegistryBackend; use auths_id::storage::git_refs::AttestationMetadata; use auths_verifier::Capability; +use auths_verifier::PublicKeyHex; pub use auths_verifier::core::Role; use auths_verifier::core::{Attestation, Ed25519PublicKey}; use auths_verifier::types::{DeviceDID, IdentityDID}; @@ -90,9 +91,9 @@ pub fn member_role_order(role: &Option) -> u8 { pub(crate) fn find_admin( backend: &dyn RegistryBackend, org_prefix: &str, - public_key_hex: &str, + public_key_hex: &PublicKeyHex, ) -> Result { - let signer_bytes = hex::decode(public_key_hex) + let signer_bytes = hex::decode(public_key_hex.as_str()) .map_err(|e| OrgError::InvalidPublicKey(format!("hex decode failed: {e}")))?; let mut found: Option = None; @@ -201,7 +202,7 @@ pub struct AddMemberCommand { /// Capability strings to grant. pub capabilities: Vec, /// Hex-encoded public key of the signing admin. - pub admin_public_key_hex: String, + pub admin_public_key_hex: PublicKeyHex, /// Keychain alias of the admin's signing key. pub signer_alias: KeyAlias, /// Optional note for the attestation. @@ -237,7 +238,7 @@ pub struct RevokeMemberCommand { /// Ed25519 public key of the member (from existing attestation). pub member_public_key: Ed25519PublicKey, /// Hex-encoded public key of the signing admin. - pub admin_public_key_hex: String, + pub admin_public_key_hex: PublicKeyHex, /// Keychain alias of the admin's signing key. pub signer_alias: KeyAlias, /// Optional reason for revocation. @@ -253,7 +254,7 @@ pub struct UpdateCapabilitiesCommand { /// New capability strings to replace the existing set. pub capabilities: Vec, /// Hex-encoded public key of the admin performing the update. - pub public_key_hex: String, + pub public_key_hex: PublicKeyHex, } /// Command to atomically update a member's role and capabilities. @@ -270,7 +271,7 @@ pub struct UpdateMemberCommand { /// New capability strings (if changing). pub capabilities: Option>, /// Hex-encoded public key of the admin performing the update. - pub admin_public_key_hex: String, + pub admin_public_key_hex: PublicKeyHex, } /// Accepts either a KERI prefix or a full DID. diff --git a/crates/auths-sdk/tests/cases/artifact.rs b/crates/auths-sdk/tests/cases/artifact.rs index 99266ccd..60080076 100644 --- a/crates/auths-sdk/tests/cases/artifact.rs +++ b/crates/auths-sdk/tests/cases/artifact.rs @@ -137,7 +137,7 @@ fn sign_artifact_with_alias_keys_produces_valid_json() { assert!(!result.digest.is_empty()); let parsed: serde_json::Value = serde_json::from_str(&result.attestation_json).unwrap(); - assert_eq!(parsed["rid"].as_str().unwrap(), result.rid); + assert_eq!(result.rid, parsed["rid"].as_str().unwrap()); assert!(parsed.get("identity_signature").is_some()); assert!(parsed.get("device_signature").is_some()); } @@ -164,7 +164,7 @@ fn sign_artifact_with_direct_device_key_produces_valid_json() { assert!(result.rid.starts_with("sha256:")); let parsed: serde_json::Value = serde_json::from_str(&result.attestation_json).unwrap(); - assert_eq!(parsed["rid"].as_str().unwrap(), result.rid); + assert_eq!(result.rid, parsed["rid"].as_str().unwrap()); } #[test] diff --git a/crates/auths-sdk/tests/cases/org.rs b/crates/auths-sdk/tests/cases/org.rs index 7303ffb4..ac800372 100644 --- a/crates/auths-sdk/tests/cases/org.rs +++ b/crates/auths-sdk/tests/cases/org.rs @@ -11,6 +11,7 @@ use auths_sdk::workflows::org::{ add_organization_member, revoke_organization_member, update_member_capabilities, }; use auths_verifier::Capability; +use auths_verifier::PublicKeyHex; use auths_verifier::clock::ClockProvider; use auths_verifier::core::{Attestation, Ed25519PublicKey, Ed25519Signature, ResourceId}; use auths_verifier::testing::MockClock; @@ -29,8 +30,8 @@ const MEMBER_PUBKEY: [u8; 32] = [ 0, 0, 0, 0, ]; -fn admin_pubkey_hex() -> String { - hex::encode(ADMIN_PUBKEY) +fn admin_pubkey_hex() -> PublicKeyHex { + PublicKeyHex::new_unchecked(hex::encode(ADMIN_PUBKEY)) } fn org_issuer() -> IdentityDID { @@ -151,7 +152,7 @@ fn find_admin_returns_not_found_when_pubkey_mismatch() { let clock = MockClock(chrono::Utc::now()); let ctx = make_ctx(&backend, &clock, &uuid, &signer, &pp); - let wrong_hex = hex::encode([0x00u8; 4]); + let wrong_hex = PublicKeyHex::new_unchecked(hex::encode([0x00u8; 32])); let result = add_organization_member( &ctx, AddMemberCommand { diff --git a/crates/auths-storage/src/git/adapter.rs b/crates/auths-storage/src/git/adapter.rs index 90bc00d1..fb2bb98e 100644 --- a/crates/auths-storage/src/git/adapter.rs +++ b/crates/auths-storage/src/git/adapter.rs @@ -646,10 +646,10 @@ impl GitRegistryBackend { for (prefix_str, state) in &state_overlay { if let Some(index) = &self.index { let indexed = auths_index::IndexedIdentity { - prefix: prefix_str.clone(), + prefix: auths_verifier::keri::Prefix::new_unchecked(prefix_str.clone()), current_keys: state.current_keys.clone(), sequence: state.sequence, - tip_said: state.last_event_said.to_string(), + tip_said: state.last_event_said.clone(), updated_at: self.clock.now(), }; #[allow(clippy::unwrap_used)] @@ -947,10 +947,10 @@ impl RegistryBackend for GitRegistryBackend { #[cfg(feature = "indexed-storage")] if let Some(index) = &self.index { let indexed = auths_index::IndexedIdentity { - prefix: prefix.to_string(), + prefix: prefix.clone(), current_keys: new_state.current_keys.clone(), sequence: new_state.sequence, - tip_said: event.said().to_string(), + tip_said: event.said().clone(), updated_at: self.clock.now(), }; // INVARIANT: Mutex poisoning is fatal by design @@ -1208,7 +1208,7 @@ impl RegistryBackend for GitRegistryBackend { #[cfg(feature = "indexed-storage")] if let Some(index) = &self.index { let indexed = auths_index::IndexedAttestation { - rid: attestation.rid.to_string(), + rid: attestation.rid.clone(), issuer_did: attestation.issuer.to_string(), device_did: attestation.subject.to_string(), git_ref: REGISTRY_REF.to_string(), @@ -1383,10 +1383,10 @@ impl RegistryBackend for GitRegistryBackend { #[cfg(feature = "indexed-storage")] if let Some(index) = &self.index { let indexed = auths_index::IndexedOrgMember { - org_prefix: org.to_string(), + org_prefix: auths_verifier::keri::Prefix::new_unchecked(org.to_string()), member_did: member.subject.to_string(), issuer_did: member.issuer.to_string(), - rid: member.rid.to_string(), + rid: member.rid.clone(), revoked_at: member.revoked_at, expires_at: member.expires_at, updated_at: member.timestamp.unwrap_or_else(|| self.clock.now()), @@ -1575,7 +1575,7 @@ impl RegistryBackend for GitRegistryBackend { role: None, capabilities: vec![], issuer: auths_core::storage::keychain::IdentityDID::new_unchecked(m.issuer_did), - rid: auths_verifier::core::ResourceId::new(m.rid), + rid: m.rid, revoked_at: m.revoked_at, expires_at: m.expires_at, timestamp: None, @@ -1726,10 +1726,10 @@ pub fn rebuild_identities_from_registry( match backend.get_key_state(&prefix_typed) { Ok(state) => { let indexed = IndexedIdentity { - prefix: state.prefix.to_string(), + prefix: state.prefix.clone(), current_keys: state.current_keys.clone(), sequence: state.sequence, - tip_said: state.last_event_said.to_string(), + tip_said: state.last_event_said.clone(), updated_at: backend.clock.now(), }; match index.upsert_identity(&indexed) { @@ -1790,10 +1790,10 @@ pub fn rebuild_org_members_from_registry( if let Ok(att) = &entry.attestation { let indexed = IndexedOrgMember { - org_prefix: org_prefix.clone(), + org_prefix: auths_verifier::keri::Prefix::new_unchecked(org_prefix.clone()), member_did: entry.did.to_string(), issuer_did: att.issuer.to_string(), - rid: att.rid.to_string(), + rid: att.rid.clone(), revoked_at: att.revoked_at, expires_at: att.expires_at, updated_at: att.timestamp.unwrap_or_else(|| backend.clock.now()), diff --git a/crates/auths-storage/src/git/attestation_adapter.rs b/crates/auths-storage/src/git/attestation_adapter.rs index 38c7ee60..18746272 100644 --- a/crates/auths-storage/src/git/attestation_adapter.rs +++ b/crates/auths-storage/src/git/attestation_adapter.rs @@ -212,7 +212,7 @@ impl AttestationSink for RegistryAttestationStorage { #[allow(clippy::disallowed_methods)] // Timestamp fallback for missing attestation timestamp let indexed = IndexedAttestation { - rid: attestation.rid.to_string(), + rid: attestation.rid.clone(), issuer_did: attestation.issuer.to_string(), device_did: attestation.subject.to_string(), git_ref, diff --git a/crates/auths-verifier/src/core.rs b/crates/auths-verifier/src/core.rs index 9d6be1c5..968ac9bf 100644 --- a/crates/auths-verifier/src/core.rs +++ b/crates/auths-verifier/src/core.rs @@ -664,7 +664,7 @@ pub struct IdentityBundle { /// The DID of the identity (e.g., `"did:keri:..."`) pub identity_did: IdentityDID, /// The public key in hex format for signature verification - pub public_key_hex: String, + pub public_key_hex: PublicKeyHex, /// Chain of attestations linking the signing key to the identity pub attestation_chain: Vec, /// UTC timestamp when this bundle was created @@ -1167,6 +1167,7 @@ pub enum PublicKeyHexError { /// /// Use `to_ed25519()` to convert to the byte-array `Ed25519PublicKey` type. #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] #[repr(transparent)] #[serde(try_from = "String")] pub struct PublicKeyHex(String); @@ -1815,7 +1816,7 @@ mod tests { fn identity_bundle_serializes_correctly() { let bundle = IdentityBundle { identity_did: IdentityDID::new_unchecked("did:keri:test123"), - public_key_hex: "aabbccdd".to_string(), + public_key_hex: PublicKeyHex::new_unchecked("aabbccdd"), attestation_chain: vec![], bundle_timestamp: DateTime::parse_from_rfc3339("2099-01-01T00:00:00Z") .unwrap() @@ -1835,7 +1836,7 @@ mod tests { fn identity_bundle_deserializes_correctly() { let json = r#"{ "identity_did": "did:keri:abc123", - "public_key_hex": "112233", + "public_key_hex": "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20", "attestation_chain": [], "bundle_timestamp": "2099-01-01T00:00:00Z", "max_valid_for_secs": 86400 @@ -1844,7 +1845,10 @@ mod tests { let bundle: IdentityBundle = serde_json::from_str(json).unwrap(); assert_eq!(bundle.identity_did.as_str(), "did:keri:abc123"); - assert_eq!(bundle.public_key_hex, "112233"); + assert_eq!( + bundle.public_key_hex.as_str(), + "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20" + ); assert!(bundle.attestation_chain.is_empty()); } @@ -1874,7 +1878,9 @@ mod tests { let original = IdentityBundle { identity_did: IdentityDID::new_unchecked("did:keri:Eexample"), - public_key_hex: "deadbeef".to_string(), + public_key_hex: PublicKeyHex::new_unchecked( + "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", + ), attestation_chain: vec![attestation], bundle_timestamp: DateTime::parse_from_rfc3339("2099-01-01T00:00:00Z") .unwrap() From 303e3ae766781d4460dd5a20416de1c600333edc Mon Sep 17 00:00:00 2001 From: bordumb Date: Wed, 11 Mar 2026 19:16:55 +0000 Subject: [PATCH 3/9] fn-63.3 + fn-63.5: thread CommitOid, DID types, and PublicKeyHex across crates --- .../src/commands/device/pair/common.rs | 2 +- crates/auths-core/src/witness/server.rs | 15 ++++--- crates/auths-id/src/identity/helpers.rs | 4 +- crates/auths-id/src/keri/cache.rs | 18 ++++---- crates/auths-id/src/keri/incremental.rs | 2 +- crates/auths-id/src/keri/kel.rs | 2 +- crates/auths-id/src/keri/resolve.rs | 7 +-- crates/auths-id/src/storage/indexed.rs | 4 +- crates/auths-index/src/index.rs | 45 ++++++++++--------- crates/auths-index/src/rebuild.rs | 9 ++-- crates/auths-sdk/src/error.rs | 5 ++- crates/auths-sdk/src/pairing/mod.rs | 16 +++---- crates/auths-sdk/src/types.rs | 3 +- crates/auths-storage/src/git/adapter.rs | 2 +- .../src/git/attestation_adapter.rs | 2 +- crates/auths-verifier/src/core.rs | 18 ++++++-- 16 files changed, 87 insertions(+), 67 deletions(-) diff --git a/crates/auths-cli/src/commands/device/pair/common.rs b/crates/auths-cli/src/commands/device/pair/common.rs index f32018a8..54085225 100644 --- a/crates/auths-cli/src/commands/device/pair/common.rs +++ b/crates/auths-cli/src/commands/device/pair/common.rs @@ -277,7 +277,7 @@ pub(crate) fn handle_pairing_response( let decrypted = DecryptedPairingResponse { auths_dir: auths_dir.to_path_buf(), device_pubkey: device_signing_bytes, - device_did: response.device_did.to_string(), + device_did: auths_verifier::types::DeviceDID::new_unchecked(response.device_did.to_string()), device_name: response.device_name.clone(), capabilities: capabilities.to_vec(), identity_key_alias, diff --git a/crates/auths-core/src/witness/server.rs b/crates/auths-core/src/witness/server.rs index e494890c..b6f41377 100644 --- a/crates/auths-core/src/witness/server.rs +++ b/crates/auths-core/src/witness/server.rs @@ -20,6 +20,7 @@ use std::sync::Arc; use auths_crypto::SecureSeed; use auths_verifier::keri::{Prefix, Said}; +use auths_verifier::types::DeviceDID; use axum::{ Json, Router, extract::{Path as AxumPath, State}, @@ -42,7 +43,7 @@ pub struct WitnessServerState { struct WitnessServerInner { /// Witness identifier (DID) - witness_did: String, + witness_did: DeviceDID, /// Ed25519 seed for signing receipts seed: SecureSeed, /// Ed25519 public key (32 bytes) @@ -57,7 +58,7 @@ struct WitnessServerInner { #[derive(Clone)] pub struct WitnessServerConfig { /// Witness identifier (DID) - pub witness_did: String, + pub witness_did: DeviceDID, /// Ed25519 seed for signing pub keypair_seed: SecureSeed, /// Ed25519 public key (32 bytes) @@ -78,7 +79,7 @@ impl WitnessServerConfig { let (seed, public_key) = provider_bridge::generate_ed25519_keypair_sync() .map_err(|e| WitnessError::Network(format!("failed to generate keypair: {}", e)))?; - let witness_did = format!("did:key:z6Mk{}", hex::encode(&public_key[..16])); + let witness_did = DeviceDID::new_unchecked(format!("did:key:z6Mk{}", hex::encode(&public_key[..16]))); Ok(Self { witness_did, @@ -111,7 +112,7 @@ pub struct HealthResponse { /// Witness server status string. pub status: String, /// DID of this witness. - pub witness_did: String, + pub witness_did: DeviceDID, /// Number of first-seen events recorded. pub first_seen_count: usize, /// Total receipts issued. @@ -157,7 +158,7 @@ impl WitnessServerState { /// Create a new server state with in-memory storage (for testing). #[allow(clippy::disallowed_methods)] // Server constructor is a clock boundary pub fn in_memory( - witness_did: String, + witness_did: DeviceDID, seed: SecureSeed, public_key: [u8; 32], ) -> Result { @@ -182,7 +183,7 @@ impl WitnessServerState { let (seed, public_key) = provider_bridge::generate_ed25519_keypair_sync() .map_err(|e| WitnessError::Network(format!("failed to generate keypair: {}", e)))?; - let witness_did = format!("did:key:z6Mk{}", hex::encode(&public_key[..16])); + let witness_did = DeviceDID::new_unchecked(format!("did:key:z6Mk{}", hex::encode(&public_key[..16]))); let storage = WitnessStorage::in_memory()?; @@ -213,7 +214,7 @@ impl WitnessServerState { v: KERI_VERSION.into(), t: RECEIPT_TYPE.into(), d: Said::default(), - i: self.inner.witness_did.clone(), + i: self.inner.witness_did.to_string(), s: seq, a: event_said.clone(), sig: vec![], diff --git a/crates/auths-id/src/identity/helpers.rs b/crates/auths-id/src/identity/helpers.rs index ff32ccc9..d9d6f901 100644 --- a/crates/auths-id/src/identity/helpers.rs +++ b/crates/auths-id/src/identity/helpers.rs @@ -15,7 +15,7 @@ use ring::signature::Ed25519KeyPair; use crate::storage::attestation::AttestationSource; use auths_verifier::core::{Attestation, ResourceId}; -use auths_verifier::types::DeviceDID; +use auths_verifier::types::{DeviceDID, IdentityDID}; pub use crate::identity::managed::ManagedIdentity; @@ -24,7 +24,7 @@ const OID_ED25519: pkcs8::der::asn1::ObjectIdentifier = #[derive(Debug, Clone)] pub struct Identity { - pub did: String, + pub did: IdentityDID, pub rid: ResourceId, pub device_dids: Vec, } diff --git a/crates/auths-id/src/keri/cache.rs b/crates/auths-id/src/keri/cache.rs index 5d39e33d..c9e95763 100644 --- a/crates/auths-id/src/keri/cache.rs +++ b/crates/auths-id/src/keri/cache.rs @@ -12,6 +12,8 @@ //! - On any mismatch, cache is treated as a miss and full replay occurs //! - Cache files are local-only, never committed to Git or replicated +use auths_verifier::types::IdentityDID; +use auths_verifier::CommitOid; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; @@ -33,13 +35,13 @@ pub struct CachedKelState { /// Cache format version for forward compatibility pub version: u32, /// The DID this cache entry is for (authoritative, verified on load) - pub did: String, + pub did: IdentityDID, /// Sequence number of the last event pub sequence: u64, /// SAID of the tip event when this cache was validated pub validated_against_tip_said: Said, /// Git commit OID of the tip event (hex-encoded) - enables incremental validation - pub last_commit_oid: String, + pub last_commit_oid: CommitOid, /// The validated key state pub state: KeyState, /// When this cache entry was created @@ -104,10 +106,10 @@ pub fn write_kel_cache( ) -> Result<(), CacheError> { let cache = CachedKelState { version: CACHE_VERSION, - did: did.to_string(), + did: IdentityDID::new_unchecked(did), sequence: state.sequence, validated_against_tip_said: Said::new_unchecked(tip_said.to_string()), - last_commit_oid: commit_oid.to_string(), + last_commit_oid: CommitOid::new_unchecked(commit_oid), state: state.clone(), cached_at: now, }; @@ -208,7 +210,7 @@ pub fn try_load_cached_state_full(auths_home: &Path, did: &str) -> Option Result<(), io::Error> { #[derive(Debug, Clone)] pub struct CacheEntry { /// The DID this cache is for - pub did: String, + pub did: IdentityDID, /// Sequence number pub sequence: u64, /// SAID the cache was validated against pub validated_against_tip_said: Said, /// Git commit OID of the cached position - pub last_commit_oid: String, + pub last_commit_oid: CommitOid, /// When the cache was created pub cached_at: DateTime, /// Path to the cache file @@ -436,7 +438,7 @@ mod tests { let path = cache_path_for_did(dir.path(), did); let mut cache: CachedKelState = serde_json::from_str(&fs::read_to_string(&path).unwrap()).unwrap(); - cache.did = "did:keri:EWrongDid".to_string(); + cache.did = IdentityDID::new_unchecked("did:keri:EWrongDid"); fs::write(&path, serde_json::to_vec_pretty(&cache).unwrap()).unwrap(); // Try to load - should return None due to DID mismatch diff --git a/crates/auths-id/src/keri/incremental.rs b/crates/auths-id/src/keri/incremental.rs index b18eb6c5..e8f9f6d3 100644 --- a/crates/auths-id/src/keri/incremental.rs +++ b/crates/auths-id/src/keri/incremental.rs @@ -138,7 +138,7 @@ pub fn try_incremental_validation<'a>( } // Parse cached commit hash - let cached_hash = match GitKel::parse_hash(&cached.last_commit_oid) { + let cached_hash = match GitKel::parse_hash(cached.last_commit_oid.as_str()) { Ok(h) => h, Err(_) => { log::debug!("KEL cache corrupt for {}: invalid commit hash", did); diff --git a/crates/auths-id/src/keri/kel.rs b/crates/auths-id/src/keri/kel.rs index 950ea655..3ad0c57f 100644 --- a/crates/auths-id/src/keri/kel.rs +++ b/crates/auths-id/src/keri/kel.rs @@ -896,7 +896,7 @@ mod tests { &did, &cached_full.state, "EFakeSaidThatDoesNotMatchCommit", - &cached_full.last_commit_oid, + cached_full.last_commit_oid.as_str(), chrono::Utc::now(), ); diff --git a/crates/auths-id/src/keri/resolve.rs b/crates/auths-id/src/keri/resolve.rs index ad71fd1f..44779985 100644 --- a/crates/auths-id/src/keri/resolve.rs +++ b/crates/auths-id/src/keri/resolve.rs @@ -6,6 +6,7 @@ //! 3. Decoding the current public key use auths_crypto::KeriPublicKey; +use auths_verifier::types::IdentityDID; use git2::Repository; use super::types::Prefix; @@ -41,7 +42,7 @@ pub enum ResolveError { #[derive(Debug, Clone)] pub struct DidKeriResolution { /// The full DID string - pub did: String, + pub did: IdentityDID, /// The KERI prefix pub prefix: Prefix, @@ -90,7 +91,7 @@ pub fn resolve_did_keri(repo: &Repository, did: &str) -> Result>, /// Optional expiration timestamp @@ -49,8 +50,8 @@ pub struct IndexedIdentity { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct IndexedOrgMember { pub org_prefix: Prefix, - pub member_did: String, - pub issuer_did: String, + pub member_did: DeviceDID, + pub issuer_did: IdentityDID, pub rid: ResourceId, pub revoked_at: Option>, pub expires_at: Option>, @@ -277,10 +278,10 @@ impl AttestationIndex { Ok(IndexedAttestation { rid: ResourceId::new(rid), - issuer_did, - device_did, + issuer_did: IdentityDID::new_unchecked(issuer_did), + device_did: DeviceDID::new_unchecked(device_did), git_ref, - commit_oid, + commit_oid: CommitOid::new_unchecked(commit_oid), revoked_at, expires_at, updated_at, @@ -407,8 +408,8 @@ impl AttestationIndex { members.push(IndexedOrgMember { org_prefix: Prefix::new_unchecked(org_prefix), - member_did, - issuer_did, + member_did: DeviceDID::new_unchecked(member_did), + issuer_did: IdentityDID::new_unchecked(issuer_did), rid: ResourceId::new(rid), revoked_at: parse_dt(revoked_str), expires_at: parse_dt(expires_str), @@ -458,10 +459,10 @@ mod tests { ) -> IndexedAttestation { IndexedAttestation { rid: ResourceId::new(rid), - issuer_did: "did:key:issuer123".to_string(), - device_did: device.to_string(), + 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: "abc123".to_string(), + commit_oid: CommitOid::new_unchecked("abc123"), revoked_at, expires_at: Some(Utc::now() + Duration::days(30)), updated_at: Utc::now(), @@ -641,8 +642,8 @@ mod tests { let index = AttestationIndex::in_memory().unwrap(); let member = IndexedOrgMember { org_prefix: Prefix::new_unchecked("did:keri:EOrg".to_string()), - member_did: "did:key:z6MkMember1".to_string(), - issuer_did: "did:keri:EOrg".to_string(), + member_did: DeviceDID::new_unchecked("did:key:z6MkMember1"), + issuer_did: IdentityDID::new_unchecked("did:keri:EOrg"), rid: ResourceId::new("rid-member-1"), revoked_at: None, expires_at: None, @@ -662,8 +663,8 @@ mod tests { let index = AttestationIndex::in_memory().unwrap(); let member = IndexedOrgMember { org_prefix: Prefix::new_unchecked("did:keri:EOrg".to_string()), - member_did: "did:key:z6MkMember1".to_string(), - issuer_did: "did:keri:EOrg".to_string(), + member_did: DeviceDID::new_unchecked("did:key:z6MkMember1"), + issuer_did: IdentityDID::new_unchecked("did:keri:EOrg"), rid: ResourceId::new("rid-v1"), revoked_at: None, expires_at: None, @@ -673,8 +674,8 @@ mod tests { let updated = IndexedOrgMember { org_prefix: Prefix::new_unchecked("did:keri:EOrg".to_string()), - member_did: "did:key:z6MkMember1".to_string(), - issuer_did: "did:keri:EOrg".to_string(), + member_did: DeviceDID::new_unchecked("did:key:z6MkMember1"), + issuer_did: IdentityDID::new_unchecked("did:keri:EOrg"), rid: ResourceId::new("rid-v2"), revoked_at: Some(Utc::now()), expires_at: None, @@ -697,8 +698,8 @@ mod tests { index .upsert_org_member(&IndexedOrgMember { org_prefix: Prefix::new_unchecked("did:keri:EOrg".to_string()), - member_did: format!("did:key:z6MkMember{}", i), - issuer_did: "did:keri:EOrg".to_string(), + member_did: DeviceDID::new_unchecked(format!("did:key:z6MkMember{}", i)), + issuer_did: IdentityDID::new_unchecked("did:keri:EOrg"), rid: ResourceId::new(format!("rid-{}", i)), revoked_at: None, expires_at: None, diff --git a/crates/auths-index/src/rebuild.rs b/crates/auths-index/src/rebuild.rs index 40166719..96802cdc 100644 --- a/crates/auths-index/src/rebuild.rs +++ b/crates/auths-index/src/rebuild.rs @@ -1,6 +1,7 @@ use crate::error::Result; use crate::index::{AttestationIndex, IndexedAttestation}; -use auths_verifier::core::ResourceId; +use auths_verifier::core::{CommitOid, ResourceId}; +use auths_verifier::types::{DeviceDID, IdentityDID}; use chrono::Utc; use git2::Repository; use std::path::Path; @@ -119,10 +120,10 @@ fn extract_attestation_from_ref( Ok(IndexedAttestation { rid: ResourceId::new(rid), - issuer_did, - device_did, + issuer_did: IdentityDID::new_unchecked(&issuer_did), + device_did: DeviceDID::new_unchecked(&device_did), git_ref: ref_name.to_string(), - commit_oid: commit.id().to_string(), + commit_oid: CommitOid::new_unchecked(commit.id().to_string()), revoked_at, expires_at, updated_at, diff --git a/crates/auths-sdk/src/error.rs b/crates/auths-sdk/src/error.rs index a8d3b6df..ece153c4 100644 --- a/crates/auths-sdk/src/error.rs +++ b/crates/auths-sdk/src/error.rs @@ -1,4 +1,5 @@ use auths_core::error::AuthsErrorInfo; +use auths_verifier::types::DeviceDID; use thiserror::Error; /// Typed storage errors originating from the `auths-id` layer. @@ -143,14 +144,14 @@ pub enum DeviceExtensionError { #[error("no attestation found for device {device_did}")] NoAttestationFound { /// The DID of the device with no attestation. - device_did: String, + device_did: DeviceDID, }, /// The device has already been revoked. #[error("device {device_did} is already revoked")] AlreadyRevoked { /// The DID of the revoked device. - device_did: String, + device_did: DeviceDID, }, /// Creating a new attestation failed. diff --git a/crates/auths-sdk/src/pairing/mod.rs b/crates/auths-sdk/src/pairing/mod.rs index 1567b319..428a4a49 100644 --- a/crates/auths-sdk/src/pairing/mod.rs +++ b/crates/auths-sdk/src/pairing/mod.rs @@ -126,7 +126,7 @@ pub struct PairingSessionRequest { /// let response = DecryptedPairingResponse { /// auths_dir: auths_dir.to_path_buf(), /// device_pubkey: pubkey_bytes, -/// device_did: "did:key:z6Mk...".into(), +/// device_did: DeviceDID::new_unchecked("did:key:z6Mk..."), /// device_name: Some("iPhone 15".into()), /// capabilities: vec!["sign_commit".into()], /// identity_key_alias: "main".into(), @@ -137,8 +137,8 @@ pub struct DecryptedPairingResponse { pub auths_dir: PathBuf, /// Ed25519 signing public key bytes (32 bytes). pub device_pubkey: Vec, - /// DID string of the responding device. - pub device_did: String, + /// DID of the responding device. + pub device_did: DeviceDID, /// Optional human-readable device name. pub device_name: Option, /// Capability strings to grant. @@ -163,14 +163,14 @@ pub enum PairingCompletionResult { /// Pairing completed successfully with a signed attestation. Success { /// The DID of the paired device. - device_did: String, + device_did: DeviceDID, /// Optional human-readable name of the paired device. device_name: Option, }, /// Attestation creation failed; caller should fall back to raw device info storage. Fallback { /// The DID of the device that could not be fully attested. - device_did: String, + device_did: DeviceDID, /// Optional human-readable name of the device. device_name: Option, /// The error message from the failed attestation attempt. @@ -468,7 +468,7 @@ pub fn complete_pairing_from_response( identity_storage, key_storage, device_pubkey: &device_pubkey, - device_did_str: &device_did, + device_did_str: device_did.as_str(), capabilities: &capabilities, identity_key_alias: &identity_key_alias, passphrase_provider, @@ -752,7 +752,7 @@ pub async fn initiate_online_pairing( let decrypted = DecryptedPairingResponse { auths_dir: PathBuf::new(), device_pubkey: device_signing_bytes, - device_did: response.device_did.to_string(), + device_did: DeviceDID::new_unchecked(response.device_did.to_string()), device_name: response.device_name.clone(), capabilities: session.token.capabilities.clone(), identity_key_alias, @@ -844,7 +844,7 @@ pub async fn join_pairing_session( .map_err(|e| PairingError::StorageError(e.to_string()))?; Ok(PairingCompletionResult::Success { - device_did: pairing_response.device_did, + device_did: DeviceDID::new_unchecked(pairing_response.device_did), device_name: None, }) } diff --git a/crates/auths-sdk/src/types.rs b/crates/auths-sdk/src/types.rs index 15ac1649..43f953a0 100644 --- a/crates/auths-sdk/src/types.rs +++ b/crates/auths-sdk/src/types.rs @@ -1,5 +1,6 @@ use auths_core::storage::keychain::KeyAlias; use auths_verifier::Capability; +use auths_verifier::types::DeviceDID; use std::path::PathBuf; /// Policy for handling an existing identity during developer setup. @@ -586,7 +587,7 @@ pub struct DeviceExtensionConfig { /// Path to the auths registry. pub repo_path: PathBuf, /// DID of the device whose authorization to extend. - pub device_did: String, + pub device_did: DeviceDID, /// Number of days from now for the new expiration. pub days: u32, /// Keychain alias for the identity signing key. diff --git a/crates/auths-storage/src/git/adapter.rs b/crates/auths-storage/src/git/adapter.rs index fb2bb98e..a8b7ac77 100644 --- a/crates/auths-storage/src/git/adapter.rs +++ b/crates/auths-storage/src/git/adapter.rs @@ -1212,7 +1212,7 @@ impl RegistryBackend for GitRegistryBackend { issuer_did: attestation.issuer.to_string(), device_did: attestation.subject.to_string(), git_ref: REGISTRY_REF.to_string(), - commit_oid: String::new(), + commit_oid: auths_verifier::CommitOid::new_unchecked(""), revoked_at: attestation.revoked_at, expires_at: attestation.expires_at, updated_at: attestation.timestamp.unwrap_or_else(|| self.clock.now()), diff --git a/crates/auths-storage/src/git/attestation_adapter.rs b/crates/auths-storage/src/git/attestation_adapter.rs index 18746272..67d8ae77 100644 --- a/crates/auths-storage/src/git/attestation_adapter.rs +++ b/crates/auths-storage/src/git/attestation_adapter.rs @@ -216,7 +216,7 @@ impl AttestationSink for RegistryAttestationStorage { issuer_did: attestation.issuer.to_string(), device_did: attestation.subject.to_string(), git_ref, - commit_oid: String::new(), + commit_oid: auths_verifier::CommitOid::new_unchecked(""), 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-verifier/src/core.rs b/crates/auths-verifier/src/core.rs index 968ac9bf..a5e56271 100644 --- a/crates/auths-verifier/src/core.rs +++ b/crates/auths-verifier/src/core.rs @@ -984,7 +984,7 @@ pub struct ThresholdPolicy { pub signers: Vec, /// Unique identifier for this policy - pub policy_id: String, + pub policy_id: PolicyId, /// Scope of operations this policy covers (optional) #[serde(default, skip_serializing_if = "Option::is_none")] @@ -997,11 +997,11 @@ pub struct ThresholdPolicy { impl ThresholdPolicy { /// Create a new threshold policy - pub fn new(threshold: u8, signers: Vec, policy_id: String) -> Self { + pub fn new(threshold: u8, signers: Vec, policy_id: impl Into) -> Self { Self { threshold, signers, - policy_id, + policy_id: policy_id.into(), scope: None, ceremony_endpoint: None, } @@ -1316,6 +1316,18 @@ impl From<&str> for PolicyId { } } +impl PartialEq for PolicyId { + fn eq(&self, other: &str) -> bool { + self.0 == other + } +} + +impl PartialEq<&str> for PolicyId { + fn eq(&self, other: &&str) -> bool { + self.0 == *other + } +} + #[cfg(test)] mod tests { use super::*; From 57d4e08b67bf1dd1cc2ba2cce697c9d02fbd5b95 Mon Sep 17 00:00:00 2001 From: bordumb Date: Wed, 11 Mar 2026 19:22:13 +0000 Subject: [PATCH 4/9] fn-63.6: fix compilation errors, add JsonSchema derives to new newtypes --- crates/auths-cli/src/commands/device/authorization.rs | 2 +- crates/auths-cli/src/commands/witness.rs | 2 +- crates/auths-id/src/domain/keri_resolve.rs | 3 ++- crates/auths-id/src/storage/indexed.rs | 6 +++--- crates/auths-verifier/src/core.rs | 2 ++ 5 files changed, 9 insertions(+), 6 deletions(-) diff --git a/crates/auths-cli/src/commands/device/authorization.rs b/crates/auths-cli/src/commands/device/authorization.rs index aed7cbbc..f71f43c8 100644 --- a/crates/auths-cli/src/commands/device/authorization.rs +++ b/crates/auths-cli/src/commands/device/authorization.rs @@ -433,7 +433,7 @@ fn handle_extend( ) -> Result<()> { let config = auths_sdk::types::DeviceExtensionConfig { repo_path: repo_path.to_path_buf(), - device_did: device_did.to_string(), + device_did: auths_verifier::types::DeviceDID::new_unchecked(device_did), days: days as u32, identity_key_alias: KeyAlias::new_unchecked(identity_key_alias), device_key_alias: Some(KeyAlias::new_unchecked(device_key_alias)), diff --git a/crates/auths-cli/src/commands/witness.rs b/crates/auths-cli/src/commands/witness.rs index 329a9593..df650b4f 100644 --- a/crates/auths-cli/src/commands/witness.rs +++ b/crates/auths-cli/src/commands/witness.rs @@ -78,7 +78,7 @@ pub fn handle_witness(cmd: WitnessCommand, repo_opt: Option) -> Result< }; WitnessServerState::new(WitnessServerConfig { - witness_did, + witness_did: auths_verifier::types::DeviceDID::new_unchecked(witness_did), keypair_seed: seed, keypair_pubkey: pubkey, db_path, diff --git a/crates/auths-id/src/domain/keri_resolve.rs b/crates/auths-id/src/domain/keri_resolve.rs index 54694e96..1a1176eb 100644 --- a/crates/auths-id/src/domain/keri_resolve.rs +++ b/crates/auths-id/src/domain/keri_resolve.rs @@ -1,4 +1,5 @@ use auths_crypto::KeriPublicKey; +use auths_verifier::types::IdentityDID; use super::kel_port::KelPort; use crate::keri::{DidKeriResolution, Event, Prefix, ResolveError, parse_did_keri, validate_kel}; @@ -33,7 +34,7 @@ pub fn resolve_from_events( .map_err(|e| ResolveError::InvalidKeyEncoding(e.to_string()))?; Ok(DidKeriResolution { - did: did.to_string(), + did: IdentityDID::new_unchecked(did), prefix: prefix.clone(), public_key, sequence: state.sequence, diff --git a/crates/auths-id/src/storage/indexed.rs b/crates/auths-id/src/storage/indexed.rs index c96d1615..e8b97ec6 100644 --- a/crates/auths-id/src/storage/indexed.rs +++ b/crates/auths-id/src/storage/indexed.rs @@ -88,8 +88,8 @@ impl IndexedAttestationStorage { ) -> Result<(), StorageError> { let indexed = IndexedAttestation { rid: att.rid.clone(), - issuer_did: att.issuer.to_string(), - device_did: att.subject.to_string(), + issuer_did: att.issuer.clone(), + device_did: att.subject.clone(), git_ref: git_ref.to_string(), commit_oid: CommitOid::new_unchecked(commit_oid), revoked_at: att.revoked_at, @@ -153,7 +153,7 @@ impl AttestationSource for IndexedAttestationStorage { // Collect unique device DIDs from the index let mut dids: Vec = active .into_iter() - .map(|a| DeviceDID::new_unchecked(&a.device_did)) + .map(|a| a.device_did.clone()) .collect(); dids.sort_by(|a, b| a.as_str().cmp(b.as_str())); dids.dedup(); diff --git a/crates/auths-verifier/src/core.rs b/crates/auths-verifier/src/core.rs index a5e56271..6cd6bb06 100644 --- a/crates/auths-verifier/src/core.rs +++ b/crates/auths-verifier/src/core.rs @@ -1056,6 +1056,7 @@ pub enum CommitOidError { /// /// Accepts exactly 40 lowercase hex characters (SHA-1) or 64 (SHA-256). #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] #[repr(transparent)] #[serde(try_from = "String")] pub struct CommitOid(String); @@ -1276,6 +1277,7 @@ impl<'de> Deserialize<'de> for PublicKeyHex { /// No validation — wraps any `String`. Use where policy IDs are passed around /// without needing to inspect their content. #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] #[serde(transparent)] pub struct PolicyId(String); From 7c490eed0855426645760713aae071a009df22d2 Mon Sep 17 00:00:00 2001 From: bordumb Date: Wed, 11 Mar 2026 19:46:31 +0000 Subject: [PATCH 5/9] fn-64.1 + fn-64.2 + fn-64.3: clock injection in emergency.rs, org.rs, device/* --- .../src/commands/device/authorization.rs | 11 ++-- .../src/commands/device/pair/common.rs | 13 ++-- .../src/commands/device/pair/join.rs | 8 +-- .../auths-cli/src/commands/device/pair/lan.rs | 12 +++- .../auths-cli/src/commands/device/pair/mod.rs | 14 +++-- .../src/commands/device/pair/offline.rs | 3 +- .../src/commands/device/pair/online.rs | 4 +- .../src/commands/device/verify_attestation.rs | 12 ++-- crates/auths-cli/src/commands/emergency.rs | 62 ++++++++++--------- crates/auths-cli/src/commands/org.rs | 22 +++---- crates/auths-core/Cargo.toml | 2 +- 11 files changed, 94 insertions(+), 69 deletions(-) diff --git a/crates/auths-cli/src/commands/device/authorization.rs b/crates/auths-cli/src/commands/device/authorization.rs index f71f43c8..3059a4d1 100644 --- a/crates/auths-cli/src/commands/device/authorization.rs +++ b/crates/auths-cli/src/commands/device/authorization.rs @@ -202,6 +202,8 @@ pub fn handle_device( passphrase_provider: Arc, env_config: &EnvironmentConfig, ) -> Result<()> { + #[allow(clippy::disallowed_methods)] + let now = Utc::now(); let repo_path = layout::resolve_repo_path(repo_opt)?; let mut config = StorageLayoutConfig::default(); @@ -220,7 +222,7 @@ pub fn handle_device( match cmd.command { DeviceSubcommand::List { include_revoked } => { - list_devices(&repo_path, &config, include_revoked) + list_devices(now, &repo_path, &config, include_revoked) } DeviceSubcommand::Resolve { device_did } => resolve_device(&repo_path, &device_did), DeviceSubcommand::Pair(pair_cmd) => super::pair::handle_pair(pair_cmd, env_config), @@ -471,6 +473,7 @@ fn resolve_device(repo_path: &Path, device_did_str: &str) -> Result<()> { } fn list_devices( + now: chrono::DateTime, repo_path: &Path, _config: &StorageLayoutConfig, include_revoked: bool, @@ -500,7 +503,7 @@ fn list_devices( .expect("Grouped attestations should not be empty"); let verification_result = auths_id::attestation::verify::verify_with_resolver( - chrono::Utc::now(), + now, &resolver, latest, None, @@ -511,7 +514,7 @@ fn list_devices( if latest.is_revoked() { "revoked".to_string() } else if let Some(expiry) = latest.expires_at { - if Utc::now() > expiry { + if now > expiry { "expired".to_string() } else { format!("active (expires {})", expiry.date_naive()) @@ -544,7 +547,7 @@ fn list_devices( } }; - let is_inactive = latest.is_revoked() || latest.expires_at.is_some_and(|e| Utc::now() > e); + let is_inactive = latest.is_revoked() || latest.expires_at.is_some_and(|e| now > e); if !include_revoked && is_inactive { continue; } diff --git a/crates/auths-cli/src/commands/device/pair/common.rs b/crates/auths-cli/src/commands/device/pair/common.rs index 54085225..6dfce123 100644 --- a/crates/auths-cli/src/commands/device/pair/common.rs +++ b/crates/auths-cli/src/commands/device/pair/common.rs @@ -127,6 +127,7 @@ pub(crate) fn display_sas_mismatch_warning() { /// Handle a successful pairing response — verify signature, complete ECDH, create attestation. pub(crate) fn handle_pairing_response( + now: chrono::DateTime, session: &mut PairingSession, response: SubmitResponseRequest, auths_dir: &Path, @@ -229,7 +230,7 @@ pub(crate) fn handle_pairing_response( style("No local identity found at ~/.auths").yellow() ); println!(" Run 'auths init' first to create an identity."); - save_device_info(auths_dir, &response)?; + save_device_info(now, auths_dir, &response)?; return Ok(()); } @@ -324,7 +325,7 @@ pub(crate) fn handle_pairing_response( " {}", style(format!("auths device link --device-did {} ...", device_did)).dim() ); - save_device_info(auths_dir, &response)?; + save_device_info(now, auths_dir, &response)?; } } @@ -332,7 +333,11 @@ pub(crate) fn handle_pairing_response( } /// Save device info as a JSON file (fallback when attestation creation fails). -pub(crate) fn save_device_info(auths_dir: &Path, response: &SubmitResponseRequest) -> Result<()> { +pub(crate) fn save_device_info( + now: chrono::DateTime, + auths_dir: &Path, + response: &SubmitResponseRequest, +) -> Result<()> { let devices_dir = auths_dir.join("devices"); create_restricted_dir(&devices_dir)?; @@ -345,7 +350,7 @@ pub(crate) fn save_device_info(auths_dir: &Path, response: &SubmitResponseReques "signing_pubkey": response.device_signing_pubkey.as_str(), "x25519_pubkey": response.device_x25519_pubkey.as_str(), "name": response.device_name, - "paired_at": chrono::Utc::now().to_rfc3339(), + "paired_at": now.to_rfc3339(), }); write_sensitive_file(&device_file, serde_json::to_string_pretty(&device_info)?)?; diff --git a/crates/auths-cli/src/commands/device/pair/join.rs b/crates/auths-cli/src/commands/device/pair/join.rs index 6e02231f..b3fb6969 100644 --- a/crates/auths-cli/src/commands/device/pair/join.rs +++ b/crates/auths-cli/src/commands/device/pair/join.rs @@ -8,7 +8,6 @@ use auths_core::ports::pairing::PairingRelayClient; use auths_infra_http::HttpPairingRelayClient; use auths_pairing_protocol::sas; use auths_sdk::pairing::{load_device_signing_material, validate_short_code}; -use chrono::Utc; use console::style; use crate::core::provider::CliPassphraseProvider; @@ -18,6 +17,7 @@ use super::common::*; /// Join an existing pairing session using a short code. pub(crate) async fn handle_join( + now: chrono::DateTime, code: &str, registry: &str, env_config: &EnvironmentConfig, @@ -84,11 +84,11 @@ pub(crate) async fn handle_join( short_code: normalized.clone(), ephemeral_pubkey: token_data.ephemeral_pubkey.to_string(), expires_at: chrono::DateTime::from_timestamp(token_data.expires_at, 0) - .unwrap_or_else(Utc::now), + .unwrap_or(now), capabilities: token_data.capabilities.clone(), }; - if token.is_expired(Utc::now()) { + if token.is_expired(now) { anyhow::bail!("Session expired"); } @@ -96,7 +96,7 @@ pub(crate) async fn handle_join( // Create the response + ECDH let (pairing_response, shared_secret) = PairingResponse::create( - Utc::now(), + now, &token, &material.seed, &material.public_key, diff --git a/crates/auths-cli/src/commands/device/pair/lan.rs b/crates/auths-cli/src/commands/device/pair/lan.rs index b87e9b1c..dacae6d3 100644 --- a/crates/auths-cli/src/commands/device/pair/lan.rs +++ b/crates/auths-cli/src/commands/device/pair/lan.rs @@ -25,6 +25,7 @@ use super::lan_server::{LanPairingServer, detect_lan_ip}; /// 6. Wait for response /// 7. Verify + create attestation pub async fn handle_initiate_lan( + now: chrono::DateTime, no_qr: bool, no_mdns: bool, expiry_secs: u64, @@ -46,7 +47,7 @@ pub async fn handle_initiate_lan( // Generate a session token with a placeholder endpoint — we'll update it after // the server starts and we know the actual port. let mut session = PairingToken::generate_with_expiry( - chrono::Utc::now(), + now, controller_did.clone(), "http://placeholder".to_string(), // replaced below capabilities.to_vec(), @@ -186,6 +187,7 @@ pub async fn handle_initiate_lan( } handle_pairing_response( + now, &mut session, response_data, &auths_dir, @@ -212,7 +214,11 @@ pub async fn handle_initiate_lan( } /// Join a LAN pairing session by discovering it via mDNS. -pub async fn handle_join_lan(code: &str, env_config: &EnvironmentConfig) -> Result<()> { +pub async fn handle_join_lan( + now: chrono::DateTime, + code: &str, + env_config: &EnvironmentConfig, +) -> Result<()> { use auths_core::pairing::normalize_short_code; let normalized = normalize_short_code(code); @@ -254,5 +260,5 @@ pub async fn handle_join_lan(code: &str, env_config: &EnvironmentConfig) -> Resu let registry = format!("http://{}", addr); // Delegate to the standard join flow - super::join::handle_join(&normalized, ®istry, env_config).await + super::join::handle_join(now, &normalized, ®istry, env_config).await } diff --git a/crates/auths-cli/src/commands/device/pair/mod.rs b/crates/auths-cli/src/commands/device/pair/mod.rs index bea6a863..9410e75d 100644 --- a/crates/auths-cli/src/commands/device/pair/mod.rs +++ b/crates/auths-cli/src/commands/device/pair/mod.rs @@ -14,6 +14,7 @@ mod online; use anyhow::Result; use auths_core::config::EnvironmentConfig; +use chrono::Utc; use clap::Parser; /// Default registry URL for local development. @@ -73,36 +74,39 @@ pub struct PairCommand { /// | `pair --join CODE --registry`| Online join (existing) | /// | `pair --offline` | Offline mode (no network) | pub fn handle_pair(cmd: PairCommand, env_config: &EnvironmentConfig) -> Result<()> { + #[allow(clippy::disallowed_methods)] + let now = Utc::now(); match (&cmd.join, &cmd.registry, cmd.offline) { // Offline mode takes priority (None, _, true) => { - offline::handle_initiate_offline(cmd.no_qr, cmd.timeout, &cmd.capabilities) + offline::handle_initiate_offline(now, cmd.no_qr, cmd.timeout, &cmd.capabilities) } // Join with explicit registry -> online join (Some(code), Some(registry), _) => { let rt = tokio::runtime::Runtime::new()?; - rt.block_on(join::handle_join(code, registry, env_config)) + rt.block_on(join::handle_join(now, code, registry, env_config)) } // Join without registry -> LAN join via mDNS #[cfg(feature = "lan-pairing")] (Some(code), None, _) => { let rt = tokio::runtime::Runtime::new()?; - rt.block_on(lan::handle_join_lan(code, env_config)) + rt.block_on(lan::handle_join_lan(now, code, env_config)) } // Join without registry and no LAN feature -> use default registry #[cfg(not(feature = "lan-pairing"))] (Some(code), None, _) => { let rt = tokio::runtime::Runtime::new()?; - rt.block_on(join::handle_join(code, DEFAULT_REGISTRY, env_config)) + rt.block_on(join::handle_join(now, code, DEFAULT_REGISTRY, env_config)) } // Initiate with explicit registry -> online mode (None, Some(registry), _) => { let rt = tokio::runtime::Runtime::new()?; rt.block_on(online::handle_initiate_online( + now, registry, cmd.no_qr, cmd.timeout, @@ -116,6 +120,7 @@ pub fn handle_pair(cmd: PairCommand, env_config: &EnvironmentConfig) -> Result<( (None, None, false) => { let rt = tokio::runtime::Runtime::new()?; rt.block_on(lan::handle_initiate_lan( + now, cmd.no_qr, cmd.no_mdns, cmd.timeout, @@ -129,6 +134,7 @@ pub fn handle_pair(cmd: PairCommand, env_config: &EnvironmentConfig) -> Result<( (None, None, false) => { let rt = tokio::runtime::Runtime::new()?; rt.block_on(online::handle_initiate_online( + now, DEFAULT_REGISTRY, cmd.no_qr, cmd.timeout, diff --git a/crates/auths-cli/src/commands/device/pair/offline.rs b/crates/auths-cli/src/commands/device/pair/offline.rs index 8ef08e83..2a227cf2 100644 --- a/crates/auths-cli/src/commands/device/pair/offline.rs +++ b/crates/auths-cli/src/commands/device/pair/offline.rs @@ -11,6 +11,7 @@ use super::common::*; /// Initiate a pairing session without registry (for testing). pub(crate) fn handle_initiate_offline( + now: chrono::DateTime, no_qr: bool, expiry_secs: u64, capabilities: &[String], @@ -28,7 +29,7 @@ pub(crate) fn handle_initiate_offline( let expiry = chrono::Duration::seconds(expiry_secs as i64); let session = PairingToken::generate_with_expiry( - chrono::Utc::now(), + now, controller_did.clone(), "offline".to_string(), capabilities.to_vec(), diff --git a/crates/auths-cli/src/commands/device/pair/online.rs b/crates/auths-cli/src/commands/device/pair/online.rs index e86d5896..af6e93bd 100644 --- a/crates/auths-cli/src/commands/device/pair/online.rs +++ b/crates/auths-cli/src/commands/device/pair/online.rs @@ -4,7 +4,6 @@ use anyhow::{Context, Result}; use auths_core::config::EnvironmentConfig; use auths_core::pairing::{QrOptions, render_qr}; use auths_sdk::pairing::{PairingSessionParams, PairingStatus, initiate_online_pairing}; -use chrono::Utc; use console::style; use indicatif::ProgressBar; @@ -17,6 +16,7 @@ use super::common::*; /// Initiate a pairing session using the registry relay. pub(crate) async fn handle_initiate_online( + now: chrono::DateTime, registry: &str, no_qr: bool, expiry_secs: u64, @@ -114,7 +114,7 @@ pub(crate) async fn handle_initiate_online( expiry_secs, }; - match initiate_online_pairing(params, &relay, &ctx, Utc::now(), Some(&on_status)) + match initiate_online_pairing(params, &relay, &ctx, now, Some(&on_status)) .await .map_err(|e| anyhow::anyhow!("{}", e))? { diff --git a/crates/auths-cli/src/commands/device/verify_attestation.rs b/crates/auths-cli/src/commands/device/verify_attestation.rs index be8ddccf..15a5ca94 100644 --- a/crates/auths-cli/src/commands/device/verify_attestation.rs +++ b/crates/auths-cli/src/commands/device/verify_attestation.rs @@ -101,7 +101,9 @@ struct VerifyResult { /// Handle verify command. Returns Ok(()) on success, Err on error. /// Uses exit codes: 0=valid, 1=invalid, 2=error pub async fn handle_verify(cmd: VerifyCommand) -> Result<()> { - let result = run_verify(&cmd).await; + #[allow(clippy::disallowed_methods)] + let now = Utc::now(); + let result = run_verify(now, &cmd).await; match result { Ok(verify_result) => { @@ -169,7 +171,7 @@ fn effective_trust_policy(cmd: &VerifyCommand) -> TrustPolicy { /// 2. Pinned identity store /// 3. roots.json file /// 4. Trust policy (TOFU prompt or explicit rejection) -fn resolve_issuer_key(cmd: &VerifyCommand, att: &Attestation) -> Result> { +fn resolve_issuer_key(now: chrono::DateTime, cmd: &VerifyCommand, att: &Attestation) -> Result> { // 1. Direct key takes precedence if let Some(ref pk_hex) = cmd.issuer_pk { let pk_bytes = @@ -221,7 +223,7 @@ fn resolve_issuer_key(cmd: &VerifyCommand, att: &Attestation) -> Result> public_key_hex: root.public_key_hex.clone(), kel_tip_said: root.kel_tip_said.clone(), kel_sequence: None, - first_seen: Utc::now(), + first_seen: now, origin: format!("roots.json:{}", roots_path.display()), trust_level: TrustLevel::OrgPolicy, }; @@ -258,7 +260,7 @@ fn resolve_issuer_key(cmd: &VerifyCommand, att: &Attestation) -> Result> use crate::commands::verify_helpers::parse_witness_keys; -async fn run_verify(cmd: &VerifyCommand) -> Result { +async fn run_verify(now: chrono::DateTime, cmd: &VerifyCommand) -> Result { // 1. Read attestation from file or stdin let attestation_bytes = if cmd.attestation == "-" { let mut buffer = Vec::new(); @@ -283,7 +285,7 @@ async fn run_verify(cmd: &VerifyCommand) -> Result { } // 3. Resolve issuer public key - let issuer_pk_bytes = resolve_issuer_key(cmd, &att)?; + let issuer_pk_bytes = resolve_issuer_key(now, cmd, &att)?; let required_capability: Option = cmd.require_capability.as_ref().map(|cap| { cap.parse::().unwrap_or_else(|e| { diff --git a/crates/auths-cli/src/commands/emergency.rs b/crates/auths-cli/src/commands/emergency.rs index 9b3da135..a490457a 100644 --- a/crates/auths-cli/src/commands/emergency.rs +++ b/crates/auths-cli/src/commands/emergency.rs @@ -173,19 +173,21 @@ pub struct EventInfo { } /// Handle the emergency command. -pub fn handle_emergency(cmd: EmergencyCommand) -> Result<()> { +pub fn handle_emergency(cmd: EmergencyCommand, now: chrono::DateTime) -> Result<()> { match cmd.command { - Some(EmergencySubcommand::RevokeDevice(c)) => handle_revoke_device(c), - Some(EmergencySubcommand::RotateNow(c)) => handle_rotate_now(c), - Some(EmergencySubcommand::Freeze(c)) => handle_freeze(c), - Some(EmergencySubcommand::Unfreeze(c)) => handle_unfreeze(c), - Some(EmergencySubcommand::Report(c)) => handle_report(c), + Some(EmergencySubcommand::RevokeDevice(c)) => handle_revoke_device(c, now), + Some(EmergencySubcommand::RotateNow(c)) => handle_rotate_now(c, now), + Some(EmergencySubcommand::Freeze(c)) => handle_freeze(c, now), + Some(EmergencySubcommand::Unfreeze(c)) => handle_unfreeze(c, now), + Some(EmergencySubcommand::Report(c)) => handle_report(c, now), None => handle_interactive_flow(), } } /// Handle interactive emergency flow. fn handle_interactive_flow() -> Result<()> { + #[allow(clippy::disallowed_methods)] + let now = chrono::Utc::now(); let out = Output::new(); if !std::io::stdin().is_terminal() { @@ -227,7 +229,7 @@ fn handle_interactive_flow() -> Result<()> { yes: false, dry_run: false, repo: None, - }) + }, now) } 1 => { // Key exposed @@ -239,7 +241,7 @@ fn handle_interactive_flow() -> Result<()> { dry_run: false, reason: Some("Potential key exposure".to_string()), repo: None, - }) + }, now) } 2 => { // Freeze everything @@ -249,7 +251,7 @@ fn handle_interactive_flow() -> Result<()> { yes: false, dry_run: false, repo: None, - }) + }, now) } 3 => { // Generate report @@ -257,7 +259,7 @@ fn handle_interactive_flow() -> Result<()> { events: 100, output_file: None, repo: None, - }) + }, now) } _ => { out.println("Cancelled."); @@ -267,7 +269,7 @@ fn handle_interactive_flow() -> Result<()> { } /// Handle device revocation using the real revocation code path. -fn handle_revoke_device(cmd: RevokeDeviceCommand) -> Result<()> { +fn handle_revoke_device(cmd: RevokeDeviceCommand, now: chrono::DateTime) -> Result<()> { use auths_core::signing::{PassphraseProvider, StorageSigner}; use auths_core::storage::keychain::{KeyAlias, get_platform_keychain}; use auths_id::attestation::export::AttestationSink; @@ -368,7 +370,7 @@ fn handle_revoke_device(cmd: RevokeDeviceCommand) -> Result<()> { let secure_signer = StorageSigner::new(get_platform_keychain()?); - let revocation_timestamp = chrono::Utc::now(); + let revocation_timestamp = now; out.print_info("Creating signed revocation attestation..."); let identity_key_alias = KeyAlias::new_unchecked(identity_key_alias); @@ -403,7 +405,7 @@ fn handle_revoke_device(cmd: RevokeDeviceCommand) -> Result<()> { } /// Handle emergency key rotation using the real rotation code path. -fn handle_rotate_now(cmd: RotateNowCommand) -> Result<()> { +fn handle_rotate_now(cmd: RotateNowCommand, now: chrono::DateTime) -> Result<()> { use auths_core::storage::keychain::{KeyAlias, get_platform_keychain}; use auths_id::identity::rotate::rotate_keri_identity; use auths_id::storage::layout::{self, StorageLayoutConfig}; @@ -487,7 +489,7 @@ fn handle_rotate_now(cmd: RotateNowCommand) -> Result<()> { &config, keychain.as_ref(), None, - chrono::Utc::now(), + now, ) .context("Key rotation failed")?; @@ -505,7 +507,7 @@ fn handle_rotate_now(cmd: RotateNowCommand) -> Result<()> { } /// Handle freeze operation — temporarily disables all signing. -fn handle_freeze(cmd: FreezeCommand) -> Result<()> { +fn handle_freeze(cmd: FreezeCommand, now: chrono::DateTime) -> Result<()> { use auths_id::freeze::{FreezeState, load_active_freeze, parse_duration, store_freeze}; use auths_id::storage::layout; @@ -516,7 +518,7 @@ fn handle_freeze(cmd: FreezeCommand) -> Result<()> { // Parse duration let duration = parse_duration(&cmd.duration)?; - let frozen_at = chrono::Utc::now(); + let frozen_at = now; let frozen_until = frozen_at + duration; out.println(&format!( @@ -530,7 +532,7 @@ fn handle_freeze(cmd: FreezeCommand) -> Result<()> { let repo_path = layout::resolve_repo_path(cmd.repo)?; // Check for existing freeze - if let Some(existing) = load_active_freeze(&repo_path, chrono::Utc::now())? { + if let Some(existing) = load_active_freeze(&repo_path, now)? { let existing_until = existing.frozen_until; if frozen_until > existing_until { out.print_warn(&format!( @@ -593,7 +595,7 @@ fn handle_freeze(cmd: FreezeCommand) -> Result<()> { out.println("All signing operations are disabled."); out.println(&format!( "Freeze expires in: {}", - out.info(&state.expires_description(chrono::Utc::now())) + out.info(&state.expires_description(now)) )); out.newline(); out.println("To unfreeze early:"); @@ -603,7 +605,7 @@ fn handle_freeze(cmd: FreezeCommand) -> Result<()> { } /// Handle unfreeze — cancel an active freeze early. -fn handle_unfreeze(cmd: UnfreezeCommand) -> Result<()> { +fn handle_unfreeze(cmd: UnfreezeCommand, now: chrono::DateTime) -> Result<()> { use auths_id::freeze::{load_active_freeze, remove_freeze}; use auths_id::storage::layout; @@ -611,7 +613,7 @@ fn handle_unfreeze(cmd: UnfreezeCommand) -> Result<()> { let repo_path = layout::resolve_repo_path(cmd.repo)?; - match load_active_freeze(&repo_path, chrono::Utc::now())? { + match load_active_freeze(&repo_path, now)? { Some(state) => { out.println(&format!( "Active freeze until {}", @@ -643,7 +645,7 @@ fn handle_unfreeze(cmd: UnfreezeCommand) -> Result<()> { } /// Handle incident report generation. -fn handle_report(cmd: ReportCommand) -> Result<()> { +fn handle_report(cmd: ReportCommand, now: chrono::DateTime) -> Result<()> { use auths_id::identity::helpers::ManagedIdentity; use auths_id::storage::attestation::AttestationSource; use auths_id::storage::identity::IdentityStorage; @@ -675,7 +677,7 @@ fn handle_report(cmd: ReportCommand) -> Result<()> { if seen_devices.insert(did_str.clone()) { let status = if att.is_revoked() { "revoked" - } else if att.expires_at.is_some_and(|exp| exp <= chrono::Utc::now()) { + } else if att.expires_at.is_some_and(|exp| exp <= now) { "expired" } else { "active" @@ -736,7 +738,7 @@ fn handle_report(cmd: ReportCommand) -> Result<()> { recommendations.push("Check for any unexpected signing activity".to_string()); let report = IncidentReport { - generated_at: chrono::Utc::now().to_rfc3339(), + generated_at: now.to_rfc3339(), identity_did: identity_did.map(|d| d.to_string()), devices, recent_events, @@ -813,12 +815,14 @@ use crate::commands::executable::ExecutableCommand; use crate::config::CliConfig; impl ExecutableCommand for EmergencyCommand { + #[allow(clippy::disallowed_methods)] fn execute(&self, _ctx: &CliConfig) -> Result<()> { - handle_emergency(self.clone()) + handle_emergency(self.clone(), chrono::Utc::now()) } } #[cfg(test)] +#[allow(clippy::disallowed_methods)] mod tests { use super::*; @@ -859,7 +863,7 @@ mod tests { yes: true, dry_run: true, repo: Some(dir.path().to_path_buf()), - }); + }, chrono::Utc::now()); assert!(result.is_ok()); // Dry run should NOT create the freeze file @@ -874,7 +878,7 @@ mod tests { yes: true, dry_run: false, repo: Some(dir.path().to_path_buf()), - }); + }, chrono::Utc::now()); assert!(result.is_ok()); assert!(dir.path().join("freeze.json").exists()); @@ -892,7 +896,7 @@ mod tests { yes: true, dry_run: false, repo: Some(dir.path().to_path_buf()), - }); + }, chrono::Utc::now()); assert!(result.is_err()); let err_msg = result.unwrap_err().to_string(); @@ -913,7 +917,7 @@ mod tests { yes: true, dry_run: false, repo: Some(dir.path().to_path_buf()), - }) + }, chrono::Utc::now()) .unwrap(); assert!(dir.path().join("freeze.json").exists()); @@ -921,7 +925,7 @@ mod tests { handle_unfreeze(UnfreezeCommand { yes: true, repo: Some(dir.path().to_path_buf()), - }) + }, chrono::Utc::now()) .unwrap(); assert!(!dir.path().join("freeze.json").exists()); } diff --git a/crates/auths-cli/src/commands/org.rs b/crates/auths-cli/src/commands/org.rs index ae0b35f2..805f2c04 100644 --- a/crates/auths-cli/src/commands/org.rs +++ b/crates/auths-cli/src/commands/org.rs @@ -176,7 +176,7 @@ pub enum OrgSubcommand { } /// Handles `org` commands for issuing or revoking member authorizations. -pub fn handle_org(cmd: OrgCommand, ctx: &crate::config::CliConfig) -> Result<()> { +pub fn handle_org(cmd: OrgCommand, ctx: &crate::config::CliConfig, now: chrono::DateTime) -> Result<()> { let repo_path = layout::resolve_repo_path(ctx.repo_path.clone())?; let passphrase_provider = ctx.passphrase_provider.clone(); @@ -252,7 +252,7 @@ pub fn handle_org(cmd: OrgCommand, ctx: &crate::config::CliConfig) -> Result<()> let mut metadata_json = serde_json::json!({ "type": "org", "name": name, - "created_at": Utc::now().to_rfc3339() + "created_at": now.to_rfc3339() }); // Merge with additional metadata file if provided @@ -316,7 +316,6 @@ pub fn handle_org(cmd: OrgCommand, ctx: &crate::config::CliConfig) -> Result<()> })?; let org_pk_bytes = *org_resolved.public_key(); - let now = Utc::now(); let admin_capabilities = vec![ Capability::sign_commit(), Capability::sign_release(), @@ -443,7 +442,6 @@ pub fn handle_org(cmd: OrgCommand, ctx: &crate::config::CliConfig) -> Result<()> })?; let device_pk_bytes = *device_resolved.public_key(); - let now = Utc::now(); let meta = AttestationMetadata { note, timestamp: Some(now), @@ -524,7 +522,6 @@ pub fn handle_org(cmd: OrgCommand, ctx: &crate::config::CliConfig) -> Result<()> #[allow(clippy::disallowed_methods)] // INVARIANT: accepts both did:key and did:keri let subject_device_did = DeviceDID::new_unchecked(subject_did.clone()); - let now = Utc::now(); // Look up the subject's public key from existing attestations let attestation_storage = RegistryAttestationStorage::new(repo_path.clone()); @@ -577,12 +574,12 @@ pub fn handle_org(cmd: OrgCommand, ctx: &crate::config::CliConfig) -> Result<()> if let Some(list) = group.by_device.get(subject_device_did.as_str()) { for (i, att) in list.iter().enumerate() { if !include_revoked - && (att.is_revoked() || att.expires_at.is_some_and(|e| Utc::now() > e)) + && (att.is_revoked() || att.expires_at.is_some_and(|e| now > e)) { continue; } - let status = match verify_with_resolver(Utc::now(), &resolver, att, None) { + 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", @@ -592,7 +589,7 @@ pub fn handle_org(cmd: OrgCommand, ctx: &crate::config::CliConfig) -> Result<()> println!( "{i}. [{}] @ {}", status, - att.timestamp.unwrap_or(Utc::now()) + att.timestamp.unwrap_or(now) ); if let Some(note) = &att.note { println!(" 📝 {}", note); @@ -618,12 +615,12 @@ pub fn handle_org(cmd: OrgCommand, ctx: &crate::config::CliConfig) -> Result<()> continue; }; if !include_revoked - && (latest.is_revoked() || latest.expires_at.is_some_and(|e| Utc::now() > e)) + && (latest.is_revoked() || latest.expires_at.is_some_and(|e| now > e)) { continue; } - let status = match verify_with_resolver(Utc::now(), &resolver, latest, None) { + 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", @@ -871,7 +868,7 @@ pub fn handle_org(cmd: OrgCommand, ctx: &crate::config::CliConfig) -> Result<()> } // Skip expired attestations - if att.expires_at.is_some_and(|e| Utc::now() > e) && !include_revoked { + if att.expires_at.is_some_and(|e| now > e) && !include_revoked { continue; } @@ -987,7 +984,8 @@ use crate::commands::executable::ExecutableCommand; use crate::config::CliConfig; impl ExecutableCommand for OrgCommand { + #[allow(clippy::disallowed_methods)] fn execute(&self, ctx: &CliConfig) -> Result<()> { - handle_org(self.clone(), ctx) + handle_org(self.clone(), ctx, Utc::now()) } } diff --git a/crates/auths-core/Cargo.toml b/crates/auths-core/Cargo.toml index d8c4de2a..61a6705e 100644 --- a/crates/auths-core/Cargo.toml +++ b/crates/auths-core/Cargo.toml @@ -8,7 +8,7 @@ publish = true license.workspace = true repository.workspace = true homepage.workspace = true -documentation = "https://docs.rs/auths_core" +documentation = "https://docs.rs/auths-core" readme = "README.md" keywords = ["cryptography", "keychain", "ed25519", "ssh", "identity"] categories = ["cryptography", "authentication"] From 2b03be2273e3df132c5a539977fd7c5cb9ee2a9c Mon Sep 17 00:00:00 2001 From: bordumb Date: Wed, 11 Mar 2026 20:11:19 +0000 Subject: [PATCH 6/9] fn-64.4: clock injection in verify_commit, status, trust, policy, audit --- crates/auths-cli/src/commands/audit.rs | 4 +++- crates/auths-cli/src/commands/policy.rs | 18 ++++++++------- crates/auths-cli/src/commands/status.rs | 1 + crates/auths-cli/src/commands/trust.rs | 10 ++++---- .../auths-cli/src/commands/verify_commit.rs | 23 ++++++++++++------- docs/plans/launch_cleaning.md | 2 +- 6 files changed, 36 insertions(+), 22 deletions(-) diff --git a/crates/auths-cli/src/commands/audit.rs b/crates/auths-cli/src/commands/audit.rs index cef8278c..b79b3a88 100644 --- a/crates/auths-cli/src/commands/audit.rs +++ b/crates/auths-cli/src/commands/audit.rs @@ -100,7 +100,9 @@ pub struct AuditReport { } /// Handle the audit command. +#[allow(clippy::disallowed_methods)] pub fn handle_audit(cmd: AuditCommand) -> Result<()> { + let now = chrono::Utc::now(); let out = Output::new(); let since = cmd.since.as_ref().map(|s| parse_date_arg(s)).transpose()?; @@ -126,7 +128,7 @@ pub fn handle_audit(cmd: AuditCommand) -> Result<()> { let summary = summarize_commits(&commits); let unsigned_commits = summary.unsigned_commits; - let generated_at = chrono::Utc::now().to_rfc3339(); + let generated_at = now.to_rfc3339(); let repository = cmd.repo.display().to_string(); let output = match cmd.format { diff --git a/crates/auths-cli/src/commands/policy.rs b/crates/auths-cli/src/commands/policy.rs index 7649dd4c..70b22b44 100644 --- a/crates/auths-cli/src/commands/policy.rs +++ b/crates/auths-cli/src/commands/policy.rs @@ -183,12 +183,14 @@ struct TestContext { // ── Handler ───────────────────────────────────────────────────────────── +#[allow(clippy::disallowed_methods)] pub fn handle_policy(cmd: PolicyCommand) -> Result<()> { + let now = Utc::now(); match cmd.command { PolicySubcommand::Lint(lint) => handle_lint(lint), PolicySubcommand::Compile(compile) => handle_compile(compile), - PolicySubcommand::Explain(explain) => handle_explain(explain), - PolicySubcommand::Test(test) => handle_test(test), + PolicySubcommand::Explain(explain) => handle_explain(explain, now), + PolicySubcommand::Test(test) => handle_test(test, now), PolicySubcommand::Diff(diff) => handle_diff(diff), } } @@ -333,7 +335,7 @@ fn handle_compile(cmd: CompileCommand) -> Result<()> { Ok(()) } -fn handle_explain(cmd: ExplainCommand) -> Result<()> { +fn handle_explain(cmd: ExplainCommand, now: DateTime) -> Result<()> { let out = Output::new(); let limits = PolicyLimits::default(); @@ -359,7 +361,7 @@ fn handle_explain(cmd: ExplainCommand) -> Result<()> { let test_ctx: TestContext = serde_json::from_slice(&ctx_content).with_context(|| "failed to parse context JSON")?; - let eval_ctx = build_eval_context(&test_ctx)?; + let eval_ctx = build_eval_context(&test_ctx, now)?; // Evaluate let decision = auths_policy::evaluate3(&policy, &eval_ctx); @@ -393,7 +395,7 @@ fn handle_explain(cmd: ExplainCommand) -> Result<()> { Ok(()) } -fn handle_test(cmd: TestCommand) -> Result<()> { +fn handle_test(cmd: TestCommand, now: DateTime) -> Result<()> { let out = Output::new(); let limits = PolicyLimits::default(); @@ -424,7 +426,7 @@ fn handle_test(cmd: TestCommand) -> Result<()> { let mut failed = 0; for test in test_cases { - let eval_ctx = match build_eval_context(&test.context) { + let eval_ctx = match build_eval_context(&test.context, now) { Ok(ctx) => ctx, Err(e) => { results.push(TestResult { @@ -607,8 +609,8 @@ fn compute_policy_stats(expr: &CompiledExpr) -> PolicyStats { } } -fn build_eval_context(test: &TestContext) -> Result { - let mut ctx = EvalContext::try_from_strings(Utc::now(), &test.issuer, &test.subject) +fn build_eval_context(test: &TestContext, now: DateTime) -> Result { + let mut ctx = EvalContext::try_from_strings(now, &test.issuer, &test.subject) .map_err(|e| anyhow!("invalid DID: {}", e))?; ctx = ctx.revoked(test.revoked); diff --git a/crates/auths-cli/src/commands/status.rs b/crates/auths-cli/src/commands/status.rs index 5c6eea12..9ed76b4c 100644 --- a/crates/auths-cli/src/commands/status.rs +++ b/crates/auths-cli/src/commands/status.rs @@ -75,6 +75,7 @@ pub struct ExpiringDevice { } /// Handle the status command. +#[allow(clippy::disallowed_methods)] pub fn handle_status(_cmd: StatusCommand, repo: Option) -> Result<()> { let now = Utc::now(); let repo_path = resolve_repo_path(repo)?; diff --git a/crates/auths-cli/src/commands/trust.rs b/crates/auths-cli/src/commands/trust.rs index 008aedbd..0c1b4a8c 100644 --- a/crates/auths-cli/src/commands/trust.rs +++ b/crates/auths-cli/src/commands/trust.rs @@ -6,7 +6,7 @@ use crate::ux::format::{JsonResponse, Output, is_json_mode}; use anyhow::{Context, Result, anyhow}; use auths_core::trust::{PinnedIdentity, PinnedIdentityStore, TrustLevel}; use auths_verifier::PublicKeyHex; -use chrono::Utc; +use chrono::{DateTime, Utc}; use clap::{Parser, Subcommand}; use serde::Serialize; @@ -108,10 +108,12 @@ struct PinDetails { } /// Handle trust subcommands. +#[allow(clippy::disallowed_methods)] pub fn handle_trust(cmd: TrustCommand) -> Result<()> { + let now = Utc::now(); match cmd.command { TrustSubcommand::List(list_cmd) => handle_list(list_cmd), - TrustSubcommand::Pin(pin_cmd) => handle_pin(pin_cmd), + TrustSubcommand::Pin(pin_cmd) => handle_pin(pin_cmd, now), TrustSubcommand::Remove(remove_cmd) => handle_remove(remove_cmd), TrustSubcommand::Show(show_cmd) => handle_show(show_cmd), } @@ -160,7 +162,7 @@ fn handle_list(_cmd: TrustListCommand) -> Result<()> { Ok(()) } -fn handle_pin(cmd: TrustPinCommand) -> Result<()> { +fn handle_pin(cmd: TrustPinCommand, now: DateTime) -> Result<()> { let public_key_hex = PublicKeyHex::parse(&cmd.key).context("Invalid public key hex")?; let store = PinnedIdentityStore::new(PinnedIdentityStore::default_path()); @@ -180,7 +182,7 @@ fn handle_pin(cmd: TrustPinCommand) -> Result<()> { public_key_hex, kel_tip_said: cmd.kel_tip, kel_sequence: None, - first_seen: Utc::now(), + first_seen: now, origin: cmd.note.unwrap_or_else(|| "manual".to_string()), trust_level: TrustLevel::Manual, }; diff --git a/crates/auths-cli/src/commands/verify_commit.rs b/crates/auths-cli/src/commands/verify_commit.rs index 5e21544e..e807f6ec 100644 --- a/crates/auths-cli/src/commands/verify_commit.rs +++ b/crates/auths-cli/src/commands/verify_commit.rs @@ -5,7 +5,7 @@ use auths_verifier::{ IdentityBundle, VerificationReport, verify_chain, verify_chain_with_witnesses, }; use base64; -use chrono::{Duration, Utc}; +use chrono::{DateTime, Duration, Utc}; use clap::Parser; use serde::Serialize; use std::fs; @@ -113,7 +113,10 @@ impl SignersSource { /// Handle verify-commit command. /// Exit codes: 0=valid, 1=invalid/unsigned, 2=error +#[allow(clippy::disallowed_methods)] pub async fn handle_verify_commit(cmd: VerifyCommitCommand) -> Result<()> { + let now = Utc::now(); + if let Err(e) = check_ssh_keygen() { return handle_error(&cmd, 2, &format!("OpenSSH required: {}", e)); } @@ -123,7 +126,7 @@ pub async fn handle_verify_commit(cmd: VerifyCommitCommand) -> Result<()> { Err(e) => return handle_error(&cmd, 2, &e.to_string()), }; - let results = match verify_commits(&cmd, &source).await { + let results = match verify_commits(&cmd, &source, now).await { Ok(r) => r, Err(e) => return handle_error(&cmd, 2, &e.to_string()), }; @@ -203,12 +206,13 @@ fn resolve_commits(commit_spec: &str) -> Result> { async fn verify_commits( cmd: &VerifyCommitCommand, source: &SignersSource, + now: DateTime, ) -> Result> { let commits = resolve_commits(&cmd.commit)?; let mut results = Vec::with_capacity(commits.len()); for sha in &commits { - let result = verify_one_commit(cmd, source, sha).await; + let result = verify_one_commit(cmd, source, sha, now).await; results.push(result); } @@ -220,6 +224,7 @@ async fn verify_one_commit( cmd: &VerifyCommitCommand, source: &SignersSource, commit_sha: &str, + now: DateTime, ) -> VerifyCommitResult { // Resolve commit ref to SHA let sha = match resolve_commit_sha(commit_sha) { @@ -273,7 +278,7 @@ async fn verify_one_commit( // 2. Attestation chain verification (only when bundle is present) let (chain_valid, chain_report) = if let Some(bundle) = source.bundle() { - let (cv, cr, cw) = verify_bundle_chain(bundle).await; + let (cv, cr, cw) = verify_bundle_chain(bundle, now).await; warnings.extend(cw); (cv, cr) } else { @@ -331,8 +336,9 @@ async fn verify_one_commit( /// Returns (chain_valid, chain_report, warnings). async fn verify_bundle_chain( bundle: &IdentityBundle, + now: DateTime, ) -> (Option, Option, Vec) { - if let Err(e) = bundle.check_freshness(Utc::now()) { + if let Err(e) = bundle.check_freshness(now) { return ( Some(false), None, @@ -366,7 +372,7 @@ async fn verify_bundle_chain( // Scan for upcoming expiry (< 30 days) for att in &bundle.attestation_chain { if let Some(exp) = att.expires_at { - let remaining = exp - Utc::now(); + let remaining = exp - now; if remaining < Duration::zero() { // Already expired — chain_valid will be false from the report } else if remaining < Duration::days(30) { @@ -813,6 +819,7 @@ impl crate::commands::executable::ExecutableCommand for VerifyCommitCommand { } #[cfg(test)] +#[allow(clippy::disallowed_methods)] mod tests { use super::*; @@ -919,7 +926,7 @@ mod tests { bundle_timestamp: Utc::now(), max_valid_for_secs: 86400, }; - let (cv, cr, warnings) = verify_bundle_chain(&bundle).await; + let (cv, cr, warnings) = verify_bundle_chain(&bundle, Utc::now()).await; assert!(cv.is_none()); assert!(cr.is_none()); assert!(!warnings.is_empty()); @@ -953,7 +960,7 @@ mod tests { bundle_timestamp: Utc::now(), max_valid_for_secs: 86400, }; - let (cv, _cr, warnings) = verify_bundle_chain(&bundle).await; + let (cv, _cr, warnings) = verify_bundle_chain(&bundle, Utc::now()).await; assert_eq!(cv, Some(false)); assert!(warnings[0].contains("Invalid public key hex")); } diff --git a/docs/plans/launch_cleaning.md b/docs/plans/launch_cleaning.md index 3704a11f..181d0e1a 100644 --- a/docs/plans/launch_cleaning.md +++ b/docs/plans/launch_cleaning.md @@ -253,7 +253,7 @@ These are the features that separate "impressive developer tool" from "enterpris - `crates/auths-verifier/src/error.rs` - `crates/auths-cli/src/commands/executable.rs` — add error code formatting to output - New: `crates/auths-cli/src/commands/explain.rs` -- Docs: `docs/errors/` directory with one `.md` per error code +- Docs: `docs/errors/` directory with one `.md` per error code. Look into automating error docs via a similar approach in `auths/crates/xtask/src/gen_docs.rs`, and should add new `{error}.md` files if we add errors to the code --- From 87d514bcdde765b850205a403e4f24a77c9a3eef Mon Sep 17 00:00:00 2001 From: bordumb Date: Wed, 11 Mar 2026 20:23:19 +0000 Subject: [PATCH 7/9] refactor(cli): clock injection for id/*, init/prompts, main.rs, sign.rs (fn-64.5, fn-64.6) --- crates/auths-cli/src/bin/sign.rs | 4 ++- crates/auths-cli/src/commands/id/claim.rs | 4 +-- crates/auths-cli/src/commands/id/identity.rs | 8 ++--- crates/auths-cli/src/commands/id/migrate.rs | 32 ++++++++++--------- crates/auths-cli/src/commands/id/mod.rs | 2 ++ crates/auths-cli/src/commands/init/mod.rs | 9 ++++-- crates/auths-cli/src/commands/init/prompts.rs | 6 ++-- crates/auths-cli/src/main.rs | 1 + 8 files changed, 39 insertions(+), 27 deletions(-) diff --git a/crates/auths-cli/src/bin/sign.rs b/crates/auths-cli/src/bin/sign.rs index 783fffd8..b2f4403a 100644 --- a/crates/auths-cli/src/bin/sign.rs +++ b/crates/auths-cli/src/bin/sign.rs @@ -266,7 +266,9 @@ fn run_sign(args: &Args) -> Result<()> { params = params.with_repo_path(path); } - let signature_pem = CommitSigningWorkflow::execute(&ctx, params, chrono::Utc::now()) + #[allow(clippy::disallowed_methods)] + let now = chrono::Utc::now(); + let signature_pem = CommitSigningWorkflow::execute(&ctx, params, now) .map_err(anyhow::Error::new)?; let sig_path = format!("{}.sig", buffer_file.display()); diff --git a/crates/auths-cli/src/commands/id/claim.rs b/crates/auths-cli/src/commands/id/claim.rs index 42bc5680..14bcdb28 100644 --- a/crates/auths-cli/src/commands/id/claim.rs +++ b/crates/auths-cli/src/commands/id/claim.rs @@ -6,7 +6,6 @@ use auths_core::config::EnvironmentConfig; use auths_core::signing::PassphraseProvider; use auths_infra_http::{HttpGistPublisher, HttpGitHubOAuthProvider, HttpRegistryClaimClient}; use auths_sdk::workflows::platform::{GitHubClaimConfig, claim_github_identity}; -use chrono::Utc; use clap::{Parser, Subcommand}; use console::style; @@ -42,6 +41,7 @@ pub fn handle_claim( repo_path: &Path, passphrase_provider: Arc, env_config: &EnvironmentConfig, + now: chrono::DateTime, ) -> Result<()> { let ctx = build_auths_context(repo_path, env_config, Some(passphrase_provider)) .context("Failed to build auths context")?; @@ -83,7 +83,7 @@ pub fn handle_claim( ®istry_client, &ctx, config, - Utc::now(), + now, &on_device_code, )) .map_err(|e| anyhow::anyhow!("{}", e))?; diff --git a/crates/auths-cli/src/commands/id/identity.rs b/crates/auths-cli/src/commands/id/identity.rs index ecd1f943..9c34f9ca 100644 --- a/crates/auths-cli/src/commands/id/identity.rs +++ b/crates/auths-cli/src/commands/id/identity.rs @@ -1,5 +1,4 @@ use anyhow::{Context, Result, anyhow}; -use chrono::Utc; use clap::{ArgAction, Parser, Subcommand}; use ring::signature::KeyPair; use serde::Serialize; @@ -249,6 +248,7 @@ pub fn handle_id( attestation_blob_name_override: Option, passphrase_provider: Arc, env_config: &EnvironmentConfig, + now: chrono::DateTime, ) -> Result<()> { // Determine repo path using the passed Option let repo_path = layout::resolve_repo_path(repo_opt)?; @@ -608,7 +608,7 @@ pub fn handle_id( identity_did: IdentityDID::new_unchecked(identity.controller_did.to_string()), public_key_hex, attestation_chain: attestations, - bundle_timestamp: Utc::now(), + bundle_timestamp: now, max_valid_for_secs: max_age_secs, }; @@ -635,9 +635,9 @@ pub fn handle_id( } IdSubcommand::Claim(claim_cmd) => { - super::claim::handle_claim(&claim_cmd, &repo_path, passphrase_provider, env_config) + super::claim::handle_claim(&claim_cmd, &repo_path, passphrase_provider, env_config, now) } - IdSubcommand::Migrate(migrate_cmd) => super::migrate::handle_migrate(migrate_cmd), + IdSubcommand::Migrate(migrate_cmd) => super::migrate::handle_migrate(migrate_cmd, now), } } diff --git a/crates/auths-cli/src/commands/id/migrate.rs b/crates/auths-cli/src/commands/id/migrate.rs index 17a27e96..2ddb8905 100644 --- a/crates/auths-cli/src/commands/id/migrate.rs +++ b/crates/auths-cli/src/commands/id/migrate.rs @@ -136,16 +136,16 @@ pub struct SshKeyInfo { } /// Handle the migrate command. -pub fn handle_migrate(cmd: MigrateCommand) -> Result<()> { +pub fn handle_migrate(cmd: MigrateCommand, now: chrono::DateTime) -> Result<()> { match cmd.command { - MigrateSubcommand::FromGpg(gpg_cmd) => handle_from_gpg(gpg_cmd), - MigrateSubcommand::FromSsh(ssh_cmd) => handle_from_ssh(ssh_cmd), + MigrateSubcommand::FromGpg(gpg_cmd) => handle_from_gpg(gpg_cmd, now), + MigrateSubcommand::FromSsh(ssh_cmd) => handle_from_ssh(ssh_cmd, now), MigrateSubcommand::Status(status_cmd) => handle_migrate_status(status_cmd), } } /// Handle the from-gpg subcommand. -fn handle_from_gpg(cmd: FromGpgCommand) -> Result<()> { +fn handle_from_gpg(cmd: FromGpgCommand, now: chrono::DateTime) -> Result<()> { let out = Output::new(); // Check if GPG is installed @@ -231,7 +231,7 @@ fn handle_from_gpg(cmd: FromGpgCommand) -> Result<()> { } // Perform the actual migration - perform_gpg_migration(&key, &cmd, &out) + perform_gpg_migration(&key, &cmd, &out, now) } /// Check if GPG is available. @@ -344,7 +344,7 @@ fn parse_gpg_colon_output(output: &str) -> Result> { } /// Perform the actual GPG key migration. -fn perform_gpg_migration(key: &GpgKeyInfo, cmd: &FromGpgCommand, out: &Output) -> Result<()> { +fn perform_gpg_migration(key: &GpgKeyInfo, cmd: &FromGpgCommand, out: &Output, now: chrono::DateTime) -> Result<()> { use auths_core::error::AgentError; use auths_core::storage::keychain::{KeyAlias, get_platform_keychain}; use auths_id::identity::initialize::initialize_registry_identity; @@ -405,7 +405,7 @@ fn perform_gpg_migration(key: &GpgKeyInfo, cmd: &FromGpgCommand, out: &Output) - "gpg_key_id": key.key_id, "gpg_fingerprint": key.fingerprint, "gpg_user_id": key.user_id, - "created_at": chrono::Utc::now().to_rfc3339() + "created_at": now.to_rfc3339() }); // Create a simple passphrase provider that prompts if needed @@ -438,7 +438,7 @@ fn perform_gpg_migration(key: &GpgKeyInfo, cmd: &FromGpgCommand, out: &Output) - // Create cross-reference attestation out.print_info("Creating cross-reference attestation..."); - let attestation = create_gpg_cross_reference_attestation(key, &controller_did)?; + let attestation = create_gpg_cross_reference_attestation(key, &controller_did, now)?; // Save the attestation let attestation_path = repo_path.join("gpg-migration.json"); @@ -489,6 +489,7 @@ fn perform_gpg_migration(key: &GpgKeyInfo, cmd: &FromGpgCommand, out: &Output) - fn create_gpg_cross_reference_attestation( gpg_key: &GpgKeyInfo, auths_did: &str, + now: chrono::DateTime, ) -> Result { let attestation = serde_json::json!({ "version": 1, @@ -503,7 +504,7 @@ fn create_gpg_cross_reference_attestation( "did": auths_did }, "statement": "This attestation links the GPG key to the Auths identity. Both keys belong to the same entity.", - "created_at": chrono::Utc::now().to_rfc3339(), + "created_at": now.to_rfc3339(), "instructions": "To complete the cross-reference: 1) Sign this file with your GPG key using 'gpg --armor --detach-sign', 2) The Auths signature will be added automatically." }); @@ -515,7 +516,7 @@ fn create_gpg_cross_reference_attestation( // ============================================================================ /// Handle the from-ssh subcommand. -fn handle_from_ssh(cmd: FromSshCommand) -> Result<()> { +fn handle_from_ssh(cmd: FromSshCommand, now: chrono::DateTime) -> Result<()> { let out = Output::new(); // Scan for SSH keys @@ -604,7 +605,7 @@ fn handle_from_ssh(cmd: FromSshCommand) -> Result<()> { } // Perform the actual migration - perform_ssh_migration(&key, &cmd, &out) + perform_ssh_migration(&key, &cmd, &out, now) } /// List SSH keys in ~/.ssh/ @@ -742,7 +743,7 @@ fn get_ssh_key_bits(public_path: &Path) -> Result { } /// Perform the actual SSH key migration. -fn perform_ssh_migration(key: &SshKeyInfo, cmd: &FromSshCommand, out: &Output) -> Result<()> { +fn perform_ssh_migration(key: &SshKeyInfo, cmd: &FromSshCommand, out: &Output, now: chrono::DateTime) -> Result<()> { use auths_core::error::AgentError; use auths_core::storage::keychain::{KeyAlias, get_platform_keychain}; use auths_id::identity::initialize::initialize_registry_identity; @@ -798,7 +799,7 @@ fn perform_ssh_migration(key: &SshKeyInfo, cmd: &FromSshCommand, out: &Output) - "ssh_algorithm": key.algorithm, "ssh_fingerprint": key.fingerprint, "ssh_comment": key.comment, - "created_at": chrono::Utc::now().to_rfc3339() + "created_at": now.to_rfc3339() }); // Create a simple passphrase provider @@ -829,7 +830,7 @@ fn perform_ssh_migration(key: &SshKeyInfo, cmd: &FromSshCommand, out: &Output) - // Create cross-reference attestation out.print_info("Creating cross-reference attestation..."); - let attestation = create_ssh_cross_reference_attestation(key, &controller_did)?; + let attestation = create_ssh_cross_reference_attestation(key, &controller_did, now)?; // Save the attestation let attestation_path = repo_path.join("ssh-migration.json"); @@ -893,6 +894,7 @@ fn perform_ssh_migration(key: &SshKeyInfo, cmd: &FromSshCommand, out: &Output) - fn create_ssh_cross_reference_attestation( ssh_key: &SshKeyInfo, auths_did: &str, + now: chrono::DateTime, ) -> Result { let attestation = serde_json::json!({ "version": 1, @@ -907,7 +909,7 @@ fn create_ssh_cross_reference_attestation( "did": auths_did }, "statement": "This attestation links the SSH key to the Auths identity. Both keys belong to the same entity.", - "created_at": chrono::Utc::now().to_rfc3339() + "created_at": now.to_rfc3339() }); Ok(attestation) diff --git a/crates/auths-cli/src/commands/id/mod.rs b/crates/auths-cli/src/commands/id/mod.rs index 3b06c3de..6e5e12a6 100644 --- a/crates/auths-cli/src/commands/id/mod.rs +++ b/crates/auths-cli/src/commands/id/mod.rs @@ -12,6 +12,7 @@ use crate::config::CliConfig; use anyhow::Result; impl ExecutableCommand for IdCommand { + #[allow(clippy::disallowed_methods)] fn execute(&self, ctx: &CliConfig) -> Result<()> { handle_id( self.clone(), @@ -22,6 +23,7 @@ impl ExecutableCommand for IdCommand { self.overrides.attestation_blob.clone(), ctx.passphrase_provider.clone(), &ctx.env_config, + chrono::Utc::now(), ) } } diff --git a/crates/auths-cli/src/commands/init/mod.rs b/crates/auths-cli/src/commands/init/mod.rs index b3ac4beb..227a770a 100644 --- a/crates/auths-cli/src/commands/init/mod.rs +++ b/crates/auths-cli/src/commands/init/mod.rs @@ -138,7 +138,7 @@ fn resolve_interactive(cmd: &InitCommand) -> Result { /// ```ignore /// handle_init(cmd, &ctx)?; /// ``` -pub fn handle_init(cmd: InitCommand, ctx: &CliConfig) -> Result<()> { +pub fn handle_init(cmd: InitCommand, ctx: &CliConfig, now: chrono::DateTime) -> Result<()> { let out = Output::new(); let interactive = resolve_interactive(&cmd)?; @@ -155,7 +155,7 @@ pub fn handle_init(cmd: InitCommand, ctx: &CliConfig) -> Result<()> { out.println("=".repeat(40).as_str()); match profile { - InitProfile::Developer => run_developer_setup(interactive, &out, &cmd, ctx)?, + InitProfile::Developer => run_developer_setup(interactive, &out, &cmd, ctx, now)?, InitProfile::Ci => run_ci_setup(&out, ctx)?, InitProfile::Agent => run_agent_setup(interactive, &out, &cmd, ctx)?, } @@ -168,6 +168,7 @@ fn run_developer_setup( out: &Output, cmd: &InitCommand, ctx: &CliConfig, + now: chrono::DateTime, ) -> Result<()> { let mut guide = GuidedSetup::new(out, guided::developer_steps()); @@ -219,6 +220,7 @@ fn run_developer_setup( out, Arc::clone(&ctx.passphrase_provider), &ctx.env_config, + now, )? { Some((url, _username)) => { out.print_success(&format!("Proof anchored: {}", url)); @@ -332,8 +334,9 @@ fn run_agent_setup( } impl crate::commands::executable::ExecutableCommand for InitCommand { + #[allow(clippy::disallowed_methods)] fn execute(&self, ctx: &CliConfig) -> anyhow::Result<()> { - handle_init(self.clone(), ctx) + handle_init(self.clone(), ctx, chrono::Utc::now()) } } diff --git a/crates/auths-cli/src/commands/init/prompts.rs b/crates/auths-cli/src/commands/init/prompts.rs index 5534a62c..eccf3b5f 100644 --- a/crates/auths-cli/src/commands/init/prompts.rs +++ b/crates/auths-cli/src/commands/init/prompts.rs @@ -118,6 +118,7 @@ pub(crate) fn prompt_platform_verification( out: &Output, passphrase_provider: Arc, env_config: &auths_core::config::EnvironmentConfig, + now: chrono::DateTime, ) -> Result> { let items = [ "GitHub — link your GitHub identity (recommended)", @@ -132,7 +133,7 @@ pub(crate) fn prompt_platform_verification( .interact()?; match selection { - 0 => run_github_verification(out, passphrase_provider, env_config), + 0 => run_github_verification(out, passphrase_provider, env_config, now), 1 => { out.print_warn("GitLab integration is coming soon. Continuing as anonymous."); Ok(None) @@ -145,6 +146,7 @@ fn run_github_verification( out: &Output, passphrase_provider: Arc, env_config: &auths_core::config::EnvironmentConfig, + now: chrono::DateTime, ) -> Result> { use std::time::Duration; @@ -217,7 +219,7 @@ fn run_github_verification( &controller_did, &key_alias, &ctx, - chrono::Utc::now(), + now, ) .map_err(|e| anyhow::anyhow!("{e}"))?; diff --git a/crates/auths-cli/src/main.rs b/crates/auths-cli/src/main.rs index 49695839..3f505580 100644 --- a/crates/auths-cli/src/main.rs +++ b/crates/auths-cli/src/main.rs @@ -107,6 +107,7 @@ fn run() -> Result<()> { if let Some(action) = action { let status = if result.is_ok() { "success" } else { "failed" }; + #[allow(clippy::disallowed_methods)] let now = chrono::Utc::now().timestamp(); let event = auths_telemetry::build_audit_event("unknown", action, status, now); auths_telemetry::emit_telemetry(&event); From 1501504576980ccd488f34f0276fb0005bfa2550 Mon Sep 17 00:00:00 2001 From: bordumb Date: Wed, 11 Mar 2026 20:27:34 +0000 Subject: [PATCH 8/9] refactor(cli): remove blanket #![allow(clippy::disallowed_methods)], add per-site annotations (fn-64.7) --- crates/auths-cli/src/bin/sign.rs | 7 +------ crates/auths-cli/src/bin/verify.rs | 7 +------ crates/auths-cli/src/commands/device/authorization.rs | 1 + crates/auths-cli/src/commands/device/pair/common.rs | 2 ++ crates/auths-cli/src/commands/id/claim.rs | 1 + crates/auths-cli/src/commands/init/gather.rs | 1 + crates/auths-cli/src/commands/init/helpers.rs | 2 ++ crates/auths-cli/src/commands/init/prompts.rs | 1 + crates/auths-cli/src/commands/key.rs | 6 +++++- crates/auths-cli/src/commands/witness.rs | 1 + crates/auths-cli/src/factories/mod.rs | 1 + crates/auths-cli/src/lib.rs | 9 ++------- crates/auths-cli/src/main.rs | 9 ++------- crates/auths-cli/src/ux/format.rs | 1 + 14 files changed, 22 insertions(+), 27 deletions(-) diff --git a/crates/auths-cli/src/bin/sign.rs b/crates/auths-cli/src/bin/sign.rs index b2f4403a..3f75aa64 100644 --- a/crates/auths-cli/src/bin/sign.rs +++ b/crates/auths-cli/src/bin/sign.rs @@ -1,9 +1,4 @@ -#![allow( - clippy::print_stdout, - clippy::print_stderr, - clippy::disallowed_methods, - clippy::exit -)] +#![allow(clippy::print_stdout, clippy::print_stderr, clippy::exit)] //! auths-sign: Git SSH signing program compatible with `gpg.ssh.program` //! //! Git calls this binary with ssh-keygen compatible arguments: diff --git a/crates/auths-cli/src/bin/verify.rs b/crates/auths-cli/src/bin/verify.rs index a722b459..24376c0a 100644 --- a/crates/auths-cli/src/bin/verify.rs +++ b/crates/auths-cli/src/bin/verify.rs @@ -1,9 +1,4 @@ -#![allow( - clippy::print_stdout, - clippy::print_stderr, - clippy::disallowed_methods, - clippy::exit -)] +#![allow(clippy::print_stdout, clippy::print_stderr, clippy::exit)] //! auths-verify: SSH signature verification for Auths identities //! //! Supports two modes: diff --git a/crates/auths-cli/src/commands/device/authorization.rs b/crates/auths-cli/src/commands/device/authorization.rs index 3059a4d1..53294478 100644 --- a/crates/auths-cli/src/commands/device/authorization.rs +++ b/crates/auths-cli/src/commands/device/authorization.rs @@ -435,6 +435,7 @@ fn handle_extend( ) -> Result<()> { let config = auths_sdk::types::DeviceExtensionConfig { repo_path: repo_path.to_path_buf(), + #[allow(clippy::disallowed_methods)] // INVARIANT: device_did from CLI arg validated upstream device_did: auths_verifier::types::DeviceDID::new_unchecked(device_did), days: days as u32, identity_key_alias: KeyAlias::new_unchecked(identity_key_alias), diff --git a/crates/auths-cli/src/commands/device/pair/common.rs b/crates/auths-cli/src/commands/device/pair/common.rs index 6dfce123..fb6baac1 100644 --- a/crates/auths-cli/src/commands/device/pair/common.rs +++ b/crates/auths-cli/src/commands/device/pair/common.rs @@ -278,6 +278,7 @@ pub(crate) fn handle_pairing_response( let decrypted = DecryptedPairingResponse { auths_dir: auths_dir.to_path_buf(), device_pubkey: device_signing_bytes, + #[allow(clippy::disallowed_methods)] // INVARIANT: device_did from pairing protocol response device_did: auths_verifier::types::DeviceDID::new_unchecked(response.device_did.to_string()), device_name: response.device_name.clone(), capabilities: capabilities.to_vec(), @@ -364,6 +365,7 @@ pub(crate) fn save_device_info( } /// Get the hostname of this machine for device naming. +#[allow(clippy::disallowed_methods)] // CLI boundary: hostname from env pub(crate) fn hostname() -> String { std::env::var("HOSTNAME") .or_else(|_| std::env::var("HOST")) diff --git a/crates/auths-cli/src/commands/id/claim.rs b/crates/auths-cli/src/commands/id/claim.rs index 14bcdb28..8bfcad6f 100644 --- a/crates/auths-cli/src/commands/id/claim.rs +++ b/crates/auths-cli/src/commands/id/claim.rs @@ -16,6 +16,7 @@ use super::register::DEFAULT_REGISTRY_URL; const DEFAULT_GITHUB_CLIENT_ID: &str = "Ov23lio2CiTHBjM2uIL4"; +#[allow(clippy::disallowed_methods)] // CLI boundary: optional env override fn github_client_id() -> String { std::env::var("AUTHS_GITHUB_CLIENT_ID").unwrap_or_else(|_| DEFAULT_GITHUB_CLIENT_ID.to_string()) } diff --git a/crates/auths-cli/src/commands/init/gather.rs b/crates/auths-cli/src/commands/init/gather.rs index 256b74fb..9d1a35fd 100644 --- a/crates/auths-cli/src/commands/init/gather.rs +++ b/crates/auths-cli/src/commands/init/gather.rs @@ -69,6 +69,7 @@ pub(crate) fn gather_ci_config( out.newline(); let registry_path = std::env::current_dir()?.join(".auths-ci"); + #[allow(clippy::disallowed_methods)] // CLI boundary: CI passphrase from env let passphrase = std::env::var("AUTHS_PASSPHRASE").unwrap_or_else(|_| "Ci-ephemeral-pass1!".to_string()); diff --git a/crates/auths-cli/src/commands/init/helpers.rs b/crates/auths-cli/src/commands/init/helpers.rs index 590eb46e..d579f57d 100644 --- a/crates/auths-cli/src/commands/init/helpers.rs +++ b/crates/auths-cli/src/commands/init/helpers.rs @@ -76,6 +76,7 @@ pub(crate) fn parse_git_version(version_str: &str) -> Result<(u32, u32, u32)> { } } +#[allow(clippy::disallowed_methods)] // CLI boundary: CI env detection pub(crate) fn detect_ci_environment() -> Option { if std::env::var("GITHUB_ACTIONS").is_ok() { Some("GitHub Actions".to_string()) @@ -207,6 +208,7 @@ pub(crate) fn select_agent_capabilities( // --- Shell Completion Helpers --- +#[allow(clippy::disallowed_methods)] // CLI boundary: shell detection pub(crate) fn detect_shell() -> Option { std::env::var("SHELL").ok().and_then(|shell_path| { if shell_path.contains("zsh") { diff --git a/crates/auths-cli/src/commands/init/prompts.rs b/crates/auths-cli/src/commands/init/prompts.rs index eccf3b5f..8d8514e4 100644 --- a/crates/auths-cli/src/commands/init/prompts.rs +++ b/crates/auths-cli/src/commands/init/prompts.rs @@ -156,6 +156,7 @@ fn run_github_verification( use auths_sdk::workflows::platform::create_signed_platform_claim; const GITHUB_CLIENT_ID: &str = "Ov23lio2CiTHBjM2uIL4"; + #[allow(clippy::disallowed_methods)] // CLI boundary: optional env override let client_id = std::env::var("AUTHS_GITHUB_CLIENT_ID").unwrap_or_else(|_| GITHUB_CLIENT_ID.to_string()); diff --git a/crates/auths-cli/src/commands/key.rs b/crates/auths-cli/src/commands/key.rs index 6f3efc83..ccda817c 100644 --- a/crates/auths-cli/src/commands/key.rs +++ b/crates/auths-cli/src/commands/key.rs @@ -318,6 +318,7 @@ fn key_import(alias: &str, seed_file_path: &PathBuf, controller_did: &IdentityDI let seed: [u8; 32] = seed_bytes.try_into().expect("validated 32 bytes above"); let seed = Zeroizing::new(seed); + #[allow(clippy::disallowed_methods)] // CLI boundary: passphrase from env let passphrase = if let Ok(env_pass) = std::env::var("AUTHS_PASSPHRASE") { Zeroizing::new(env_pass) } else { @@ -379,7 +380,10 @@ fn key_copy_backend( // Wrap immediately in Zeroizing so the heap allocation is cleared on drop. let password: Zeroizing = dst_passphrase .map(|s| Zeroizing::new(s.to_string())) - .or_else(|| std::env::var("AUTHS_PASSPHRASE").ok().map(Zeroizing::new)) + .or_else(|| { + #[allow(clippy::disallowed_methods)] // CLI boundary: passphrase from env + std::env::var("AUTHS_PASSPHRASE").ok().map(Zeroizing::new) + }) .ok_or_else(|| { anyhow!( "Passphrase required for file backend. \ diff --git a/crates/auths-cli/src/commands/witness.rs b/crates/auths-cli/src/commands/witness.rs index df650b4f..057e4a27 100644 --- a/crates/auths-cli/src/commands/witness.rs +++ b/crates/auths-cli/src/commands/witness.rs @@ -78,6 +78,7 @@ pub fn handle_witness(cmd: WitnessCommand, repo_opt: Option) -> Result< }; WitnessServerState::new(WitnessServerConfig { + #[allow(clippy::disallowed_methods)] // INVARIANT: witness_did derived from keypair witness_did: auths_verifier::types::DeviceDID::new_unchecked(witness_did), keypair_seed: seed, keypair_pubkey: pubkey, diff --git a/crates/auths-cli/src/factories/mod.rs b/crates/auths-cli/src/factories/mod.rs index 24d069a8..ddf5a7f7 100644 --- a/crates/auths-cli/src/factories/mod.rs +++ b/crates/auths-cli/src/factories/mod.rs @@ -83,6 +83,7 @@ pub fn init_audit_sinks() -> Option { Err(_) => return None, }; let config = load_audit_config(&audit_path); + #[allow(clippy::disallowed_methods)] // CLI boundary: audit config reads env vars let sinks = build_sinks_from_config(&config, |name| std::env::var(name).ok()); if sinks.is_empty() { return None; diff --git a/crates/auths-cli/src/lib.rs b/crates/auths-cli/src/lib.rs index 79526e9e..adbb8942 100644 --- a/crates/auths-cli/src/lib.rs +++ b/crates/auths-cli/src/lib.rs @@ -1,10 +1,5 @@ -// CLI is the presentation boundary — Utc::now(), env::var, and printing are expected here. -#![allow( - clippy::print_stdout, - clippy::print_stderr, - clippy::disallowed_methods, - clippy::exit -)] +// CLI is the presentation boundary — printing and exit are expected here. +#![allow(clippy::print_stdout, clippy::print_stderr, clippy::exit)] pub mod adapters; pub mod cli; pub mod commands; diff --git a/crates/auths-cli/src/main.rs b/crates/auths-cli/src/main.rs index 3f505580..0d202ac4 100644 --- a/crates/auths-cli/src/main.rs +++ b/crates/auths-cli/src/main.rs @@ -1,10 +1,5 @@ -// CLI is the presentation boundary — Utc::now(), env::var, and printing are expected here. -#![allow( - clippy::print_stdout, - clippy::print_stderr, - clippy::disallowed_methods, - clippy::exit -)] +// CLI is the presentation boundary — printing and exit are expected here. +#![allow(clippy::print_stdout, clippy::print_stderr, clippy::exit)] use anyhow::Result; use clap::Parser; diff --git a/crates/auths-cli/src/ux/format.rs b/crates/auths-cli/src/ux/format.rs index 21a012e7..80f67a46 100644 --- a/crates/auths-cli/src/ux/format.rs +++ b/crates/auths-cli/src/ux/format.rs @@ -125,6 +125,7 @@ impl Output { } // Respect NO_COLOR env var + #[allow(clippy::disallowed_methods)] // CLI boundary: NO_COLOR convention if std::env::var("NO_COLOR").is_ok() { return false; } From f9b49086e79d8184d71524fe6e3f267cbec3abda Mon Sep 17 00:00:00 2001 From: bordumb Date: Wed, 11 Mar 2026 21:59:42 +0000 Subject: [PATCH 9/9] refactor: update DID types --- Cargo.lock | 1 + crates/auths-cli/src/bin/sign.rs | 4 +- .../auths-cli/src/commands/artifact/verify.rs | 6 +- .../src/commands/device/authorization.rs | 8 +- .../src/commands/device/pair/join.rs | 3 +- .../src/commands/device/verify_attestation.rs | 6 +- crates/auths-cli/src/commands/emergency.rs | 142 +++++++++------ crates/auths-cli/src/commands/id/migrate.rs | 14 +- crates/auths-cli/src/commands/init/mod.rs | 6 +- crates/auths-cli/src/commands/org.rs | 12 +- .../auths-cli/src/commands/verify_commit.rs | 2 +- crates/auths-cli/tests/cases/verify.rs | 5 +- crates/auths-core/src/policy/device.rs | 6 +- crates/auths-core/src/policy/org.rs | 6 +- crates/auths-core/src/witness/server.rs | 6 +- crates/auths-id/src/attestation/create.rs | 14 +- crates/auths-id/src/attestation/revoke.rs | 7 +- crates/auths-id/src/attestation/verify.rs | 5 +- .../src/domain/attestation_message.rs | 5 +- crates/auths-id/src/keri/cache.rs | 2 +- crates/auths-id/src/policy/mod.rs | 7 +- crates/auths-id/src/storage/indexed.rs | 9 +- .../auths-id/src/storage/registry/backend.rs | 2 +- .../src/storage/registry/org_member.rs | 13 +- crates/auths-id/src/testing/fakes/registry.rs | 2 +- crates/auths-id/src/testing/fixtures.rs | 5 +- crates/auths-index/src/index.rs | 2 +- crates/auths-policy/Cargo.toml | 1 + crates/auths-policy/src/compile.rs | 22 +-- crates/auths-policy/src/types.rs | 130 +------------- crates/auths-radicle/src/attestation.rs | 6 +- crates/auths-radicle/src/verify.rs | 3 +- crates/auths-radicle/tests/cases/helpers.rs | 4 +- crates/auths-sdk/src/workflows/org.rs | 6 +- crates/auths-sdk/tests/cases/device.rs | 4 +- crates/auths-sdk/tests/cases/org.rs | 8 +- crates/auths-storage/src/git/adapter.rs | 104 +++++------ .../src/git/attestation_adapter.rs | 10 +- crates/auths-verifier/src/core.rs | 24 +-- crates/auths-verifier/src/lib.rs | 4 +- crates/auths-verifier/src/types.rs | 170 ++++++++++++++++++ crates/auths-verifier/src/verify.rs | 10 +- .../tests/cases/expiration_skew.rs | 4 +- .../tests/cases/kel_verification.rs | 2 +- .../tests/cases/proptest_core.rs | 20 ++- .../tests/cases/revocation_adversarial.rs | 4 +- .../tests/cases/serialization_pinning.rs | 8 +- docs/technical/did.md | 0 packages/auths-node/src/artifact.rs | 2 +- packages/auths-node/src/device.rs | 3 +- packages/auths-node/src/org.rs | 9 +- packages/auths-node/src/trust.rs | 7 +- packages/auths-python/Cargo.lock | 1 + packages/auths-python/src/artifact_sign.rs | 2 +- packages/auths-python/src/device_ext.rs | 3 +- packages/auths-python/src/org.rs | 10 +- packages/auths-python/src/trust.rs | 9 +- 57 files changed, 498 insertions(+), 392 deletions(-) create mode 100644 docs/technical/did.md diff --git a/Cargo.lock b/Cargo.lock index 92c83750..ad4d9921 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -630,6 +630,7 @@ dependencies = [ name = "auths-policy" version = "0.0.1-rc.8" dependencies = [ + "auths-verifier", "blake3", "chrono", "serde", diff --git a/crates/auths-cli/src/bin/sign.rs b/crates/auths-cli/src/bin/sign.rs index 3f75aa64..bc232067 100644 --- a/crates/auths-cli/src/bin/sign.rs +++ b/crates/auths-cli/src/bin/sign.rs @@ -263,8 +263,8 @@ fn run_sign(args: &Args) -> Result<()> { #[allow(clippy::disallowed_methods)] let now = chrono::Utc::now(); - let signature_pem = CommitSigningWorkflow::execute(&ctx, params, now) - .map_err(anyhow::Error::new)?; + let signature_pem = + CommitSigningWorkflow::execute(&ctx, params, now).map_err(anyhow::Error::new)?; let sig_path = format!("{}.sig", buffer_file.display()); fs::write(&sig_path, &signature_pem) diff --git a/crates/auths-cli/src/commands/artifact/verify.rs b/crates/auths-cli/src/commands/artifact/verify.rs index d428624b..ffa04743 100644 --- a/crates/auths-cli/src/commands/artifact/verify.rs +++ b/crates/auths-cli/src/commands/artifact/verify.rs @@ -6,7 +6,7 @@ use std::path::{Path, PathBuf}; use auths_verifier::core::Attestation; use auths_verifier::witness::{WitnessQuorum, WitnessReceipt, WitnessVerifyConfig}; use auths_verifier::{ - Capability, IdentityBundle, IdentityDID, VerificationReport, verify_chain, + CanonicalDid, Capability, IdentityBundle, VerificationReport, verify_chain, verify_chain_with_capability, verify_chain_with_witnesses, }; @@ -209,7 +209,7 @@ pub async fn handle_verify( fn resolve_identity_key( identity_bundle: &Option, attestation: &Attestation, -) -> Result<(Vec, IdentityDID)> { +) -> Result<(Vec, CanonicalDid)> { if let Some(bundle_path) = identity_bundle { let bundle_content = fs::read_to_string(bundle_path) .with_context(|| format!("Failed to read identity bundle: {:?}", bundle_path))?; @@ -217,7 +217,7 @@ fn resolve_identity_key( .with_context(|| format!("Failed to parse identity bundle: {:?}", bundle_path))?; let pk = hex::decode(bundle.public_key_hex.as_str()) .context("Invalid public key hex in bundle")?; - Ok((pk, bundle.identity_did)) + Ok((pk, bundle.identity_did.into())) } else { // Resolve public key from the issuer DID let issuer = &attestation.issuer; diff --git a/crates/auths-cli/src/commands/device/authorization.rs b/crates/auths-cli/src/commands/device/authorization.rs index 53294478..ab6d85c1 100644 --- a/crates/auths-cli/src/commands/device/authorization.rs +++ b/crates/auths-cli/src/commands/device/authorization.rs @@ -503,12 +503,8 @@ fn list_devices( .last() .expect("Grouped attestations should not be empty"); - let verification_result = auths_id::attestation::verify::verify_with_resolver( - now, - &resolver, - latest, - None, - ); + let verification_result = + auths_id::attestation::verify::verify_with_resolver(now, &resolver, latest, None); let status_string = match verification_result { Ok(()) => { diff --git a/crates/auths-cli/src/commands/device/pair/join.rs b/crates/auths-cli/src/commands/device/pair/join.rs index b3fb6969..0e0ca9b4 100644 --- a/crates/auths-cli/src/commands/device/pair/join.rs +++ b/crates/auths-cli/src/commands/device/pair/join.rs @@ -83,8 +83,7 @@ pub(crate) async fn handle_join( endpoint: registry.to_string(), short_code: normalized.clone(), ephemeral_pubkey: token_data.ephemeral_pubkey.to_string(), - expires_at: chrono::DateTime::from_timestamp(token_data.expires_at, 0) - .unwrap_or(now), + expires_at: chrono::DateTime::from_timestamp(token_data.expires_at, 0).unwrap_or(now), capabilities: token_data.capabilities.clone(), }; diff --git a/crates/auths-cli/src/commands/device/verify_attestation.rs b/crates/auths-cli/src/commands/device/verify_attestation.rs index 15a5ca94..b270b27a 100644 --- a/crates/auths-cli/src/commands/device/verify_attestation.rs +++ b/crates/auths-cli/src/commands/device/verify_attestation.rs @@ -171,7 +171,11 @@ fn effective_trust_policy(cmd: &VerifyCommand) -> TrustPolicy { /// 2. Pinned identity store /// 3. roots.json file /// 4. Trust policy (TOFU prompt or explicit rejection) -fn resolve_issuer_key(now: chrono::DateTime, cmd: &VerifyCommand, att: &Attestation) -> Result> { +fn resolve_issuer_key( + now: chrono::DateTime, + cmd: &VerifyCommand, + att: &Attestation, +) -> Result> { // 1. Direct key takes precedence if let Some(ref pk_hex) = cmd.issuer_pk { let pk_bytes = diff --git a/crates/auths-cli/src/commands/emergency.rs b/crates/auths-cli/src/commands/emergency.rs index a490457a..33fc3850 100644 --- a/crates/auths-cli/src/commands/emergency.rs +++ b/crates/auths-cli/src/commands/emergency.rs @@ -222,44 +222,56 @@ fn handle_interactive_flow() -> Result<()> { 0 => { // Device lost/stolen out.print_info("Starting device revocation flow..."); - handle_revoke_device(RevokeDeviceCommand { - device: None, - identity_key_alias: None, - note: None, - yes: false, - dry_run: false, - repo: None, - }, now) + handle_revoke_device( + RevokeDeviceCommand { + device: None, + identity_key_alias: None, + note: None, + yes: false, + dry_run: false, + repo: None, + }, + now, + ) } 1 => { // Key exposed out.print_info("Starting key rotation flow..."); - handle_rotate_now(RotateNowCommand { - current_alias: None, - next_alias: None, - yes: false, - dry_run: false, - reason: Some("Potential key exposure".to_string()), - repo: None, - }, now) + handle_rotate_now( + RotateNowCommand { + current_alias: None, + next_alias: None, + yes: false, + dry_run: false, + reason: Some("Potential key exposure".to_string()), + repo: None, + }, + now, + ) } 2 => { // Freeze everything out.print_warn("Starting freeze flow..."); - handle_freeze(FreezeCommand { - duration: "24h".to_string(), - yes: false, - dry_run: false, - repo: None, - }, now) + handle_freeze( + FreezeCommand { + duration: "24h".to_string(), + yes: false, + dry_run: false, + repo: None, + }, + now, + ) } 3 => { // Generate report - handle_report(ReportCommand { - events: 100, - output_file: None, - repo: None, - }, now) + handle_report( + ReportCommand { + events: 100, + output_file: None, + repo: None, + }, + now, + ) } _ => { out.println("Cancelled."); @@ -269,7 +281,10 @@ fn handle_interactive_flow() -> Result<()> { } /// Handle device revocation using the real revocation code path. -fn handle_revoke_device(cmd: RevokeDeviceCommand, now: chrono::DateTime) -> Result<()> { +fn handle_revoke_device( + cmd: RevokeDeviceCommand, + now: chrono::DateTime, +) -> Result<()> { use auths_core::signing::{PassphraseProvider, StorageSigner}; use auths_core::storage::keychain::{KeyAlias, get_platform_keychain}; use auths_id::attestation::export::AttestationSink; @@ -858,12 +873,15 @@ mod tests { #[test] fn test_freeze_dry_run() { let dir = tempfile::TempDir::new().unwrap(); - let result = handle_freeze(FreezeCommand { - duration: "24h".to_string(), - yes: true, - dry_run: true, - repo: Some(dir.path().to_path_buf()), - }, chrono::Utc::now()); + let result = handle_freeze( + FreezeCommand { + duration: "24h".to_string(), + yes: true, + dry_run: true, + repo: Some(dir.path().to_path_buf()), + }, + chrono::Utc::now(), + ); assert!(result.is_ok()); // Dry run should NOT create the freeze file @@ -873,12 +891,15 @@ mod tests { #[test] fn test_freeze_creates_freeze_file() { let dir = tempfile::TempDir::new().unwrap(); - let result = handle_freeze(FreezeCommand { - duration: "1h".to_string(), - yes: true, - dry_run: false, - repo: Some(dir.path().to_path_buf()), - }, chrono::Utc::now()); + let result = handle_freeze( + FreezeCommand { + duration: "1h".to_string(), + yes: true, + dry_run: false, + repo: Some(dir.path().to_path_buf()), + }, + chrono::Utc::now(), + ); assert!(result.is_ok()); assert!(dir.path().join("freeze.json").exists()); @@ -891,12 +912,15 @@ mod tests { #[test] fn test_freeze_invalid_duration() { let dir = tempfile::TempDir::new().unwrap(); - let result = handle_freeze(FreezeCommand { - duration: "invalid".to_string(), - yes: true, - dry_run: false, - repo: Some(dir.path().to_path_buf()), - }, chrono::Utc::now()); + let result = handle_freeze( + FreezeCommand { + duration: "invalid".to_string(), + yes: true, + dry_run: false, + repo: Some(dir.path().to_path_buf()), + }, + chrono::Utc::now(), + ); assert!(result.is_err()); let err_msg = result.unwrap_err().to_string(); @@ -912,20 +936,26 @@ mod tests { let dir = tempfile::TempDir::new().unwrap(); // Create a freeze - handle_freeze(FreezeCommand { - duration: "24h".to_string(), - yes: true, - dry_run: false, - repo: Some(dir.path().to_path_buf()), - }, chrono::Utc::now()) + handle_freeze( + FreezeCommand { + duration: "24h".to_string(), + yes: true, + dry_run: false, + repo: Some(dir.path().to_path_buf()), + }, + chrono::Utc::now(), + ) .unwrap(); assert!(dir.path().join("freeze.json").exists()); // Unfreeze - handle_unfreeze(UnfreezeCommand { - yes: true, - repo: Some(dir.path().to_path_buf()), - }, chrono::Utc::now()) + handle_unfreeze( + UnfreezeCommand { + yes: true, + repo: Some(dir.path().to_path_buf()), + }, + chrono::Utc::now(), + ) .unwrap(); assert!(!dir.path().join("freeze.json").exists()); } diff --git a/crates/auths-cli/src/commands/id/migrate.rs b/crates/auths-cli/src/commands/id/migrate.rs index 2ddb8905..ebaa6fb7 100644 --- a/crates/auths-cli/src/commands/id/migrate.rs +++ b/crates/auths-cli/src/commands/id/migrate.rs @@ -344,7 +344,12 @@ fn parse_gpg_colon_output(output: &str) -> Result> { } /// Perform the actual GPG key migration. -fn perform_gpg_migration(key: &GpgKeyInfo, cmd: &FromGpgCommand, out: &Output, now: chrono::DateTime) -> Result<()> { +fn perform_gpg_migration( + key: &GpgKeyInfo, + cmd: &FromGpgCommand, + out: &Output, + now: chrono::DateTime, +) -> Result<()> { use auths_core::error::AgentError; use auths_core::storage::keychain::{KeyAlias, get_platform_keychain}; use auths_id::identity::initialize::initialize_registry_identity; @@ -743,7 +748,12 @@ fn get_ssh_key_bits(public_path: &Path) -> Result { } /// Perform the actual SSH key migration. -fn perform_ssh_migration(key: &SshKeyInfo, cmd: &FromSshCommand, out: &Output, now: chrono::DateTime) -> Result<()> { +fn perform_ssh_migration( + key: &SshKeyInfo, + cmd: &FromSshCommand, + out: &Output, + now: chrono::DateTime, +) -> Result<()> { use auths_core::error::AgentError; use auths_core::storage::keychain::{KeyAlias, get_platform_keychain}; use auths_id::identity::initialize::initialize_registry_identity; diff --git a/crates/auths-cli/src/commands/init/mod.rs b/crates/auths-cli/src/commands/init/mod.rs index 227a770a..534a8cef 100644 --- a/crates/auths-cli/src/commands/init/mod.rs +++ b/crates/auths-cli/src/commands/init/mod.rs @@ -138,7 +138,11 @@ fn resolve_interactive(cmd: &InitCommand) -> Result { /// ```ignore /// handle_init(cmd, &ctx)?; /// ``` -pub fn handle_init(cmd: InitCommand, ctx: &CliConfig, now: chrono::DateTime) -> Result<()> { +pub fn handle_init( + cmd: InitCommand, + ctx: &CliConfig, + now: chrono::DateTime, +) -> Result<()> { let out = Output::new(); let interactive = resolve_interactive(&cmd)?; diff --git a/crates/auths-cli/src/commands/org.rs b/crates/auths-cli/src/commands/org.rs index 805f2c04..26a17daa 100644 --- a/crates/auths-cli/src/commands/org.rs +++ b/crates/auths-cli/src/commands/org.rs @@ -176,7 +176,11 @@ pub enum OrgSubcommand { } /// Handles `org` commands for issuing or revoking member authorizations. -pub fn handle_org(cmd: OrgCommand, ctx: &crate::config::CliConfig, now: chrono::DateTime) -> Result<()> { +pub fn handle_org( + cmd: OrgCommand, + ctx: &crate::config::CliConfig, + now: chrono::DateTime, +) -> Result<()> { let repo_path = layout::resolve_repo_path(ctx.repo_path.clone())?; let passphrase_provider = ctx.passphrase_provider.clone(); @@ -586,11 +590,7 @@ pub fn handle_org(cmd: OrgCommand, ctx: &crate::config::CliConfig, now: chrono:: Err(_) => "❌ invalid", }; - println!( - "{i}. [{}] @ {}", - status, - att.timestamp.unwrap_or(now) - ); + println!("{i}. [{}] @ {}", status, att.timestamp.unwrap_or(now)); if let Some(note) = &att.note { println!(" 📝 {}", note); } diff --git a/crates/auths-cli/src/commands/verify_commit.rs b/crates/auths-cli/src/commands/verify_commit.rs index e807f6ec..39105ee1 100644 --- a/crates/auths-cli/src/commands/verify_commit.rs +++ b/crates/auths-cli/src/commands/verify_commit.rs @@ -941,7 +941,7 @@ mod tests { attestation_chain: vec![auths_verifier::core::Attestation { version: 1, rid: "test".into(), - issuer: auths_verifier::IdentityDID::new_unchecked("did:keri:test"), + issuer: auths_verifier::CanonicalDid::new_unchecked("did:keri:test"), subject: auths_verifier::DeviceDID::new_unchecked("did:key:zTest"), device_public_key: auths_verifier::Ed25519PublicKey::from_bytes([0u8; 32]), identity_signature: auths_verifier::core::Ed25519Signature::empty(), diff --git a/crates/auths-cli/tests/cases/verify.rs b/crates/auths-cli/tests/cases/verify.rs index 6c6e0d9d..1ec71cfb 100644 --- a/crates/auths-cli/tests/cases/verify.rs +++ b/crates/auths-cli/tests/cases/verify.rs @@ -2,12 +2,11 @@ use assert_cmd::Command; use auths_crypto::testing::gen_keypair; -use auths_verifier::IdentityDID; use auths_verifier::core::{ Attestation, CanonicalAttestationData, Ed25519PublicKey, Ed25519Signature, ResourceId, canonicalize_attestation_data, }; -use auths_verifier::types::DeviceDID; +use auths_verifier::types::{CanonicalDid, DeviceDID}; use chrono::{Duration, Utc}; use ring::signature::KeyPair; use std::io::Write; @@ -22,7 +21,7 @@ fn create_signed_attestation( let mut att = Attestation { version: 1, rid: ResourceId::new("test-rid"), - issuer: IdentityDID::new_unchecked(format!( + issuer: CanonicalDid::new_unchecked(format!( "did:key:{}", hex::encode(issuer_kp.public_key().as_ref()) )), diff --git a/crates/auths-core/src/policy/device.rs b/crates/auths-core/src/policy/device.rs index 3a9372ba..012b7611 100644 --- a/crates/auths-core/src/policy/device.rs +++ b/crates/auths-core/src/policy/device.rs @@ -3,9 +3,9 @@ //! This module implements the device authorization rules that determine //! whether a device attestation grants permission for a specific action. -#[cfg(test)] -use auths_verifier::IdentityDID; use auths_verifier::core::{Attestation, Capability}; +#[cfg(test)] +use auths_verifier::types::CanonicalDid; use chrono::{DateTime, Utc}; use super::Decision; @@ -186,7 +186,7 @@ mod tests { Attestation { version: 1, rid: "test-rid".into(), - issuer: IdentityDID::new_unchecked(issuer), + issuer: CanonicalDid::new_unchecked(issuer), subject: DeviceDID::new_unchecked("did:key:z6MkTest"), 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 3a75d1f7..9a9bd20f 100644 --- a/crates/auths-core/src/policy/org.rs +++ b/crates/auths-core/src/policy/org.rs @@ -18,8 +18,6 @@ //! with filtering (role, capabilities), use the registry's `list_org_members()` //! with `MemberFilter`, then apply this policy to each result. -#[cfg(test)] -use auths_verifier::IdentityDID; use auths_verifier::core::{Attestation, Capability}; use auths_verifier::keri::Prefix; use chrono::{DateTime, Utc}; @@ -175,7 +173,7 @@ fn capability_name(cap: &Capability) -> &str { mod tests { use super::*; use auths_verifier::core::{Ed25519PublicKey, Ed25519Signature, ResourceId, Role}; - use auths_verifier::types::DeviceDID; + use auths_verifier::types::{CanonicalDid, DeviceDID}; use chrono::Duration; fn make_membership( @@ -188,7 +186,7 @@ mod tests { Attestation { version: 1, rid: ResourceId::new("membership"), - issuer: IdentityDID::new_unchecked(issuer), + issuer: CanonicalDid::new_unchecked(issuer), subject: DeviceDID::new_unchecked("did:key:z6MkMember"), device_public_key: Ed25519PublicKey::from_bytes([0u8; 32]), identity_signature: Ed25519Signature::empty(), diff --git a/crates/auths-core/src/witness/server.rs b/crates/auths-core/src/witness/server.rs index b6f41377..7ac38f0b 100644 --- a/crates/auths-core/src/witness/server.rs +++ b/crates/auths-core/src/witness/server.rs @@ -79,7 +79,8 @@ impl WitnessServerConfig { let (seed, public_key) = provider_bridge::generate_ed25519_keypair_sync() .map_err(|e| WitnessError::Network(format!("failed to generate keypair: {}", e)))?; - let witness_did = DeviceDID::new_unchecked(format!("did:key:z6Mk{}", hex::encode(&public_key[..16]))); + let witness_did = + DeviceDID::new_unchecked(format!("did:key:z6Mk{}", hex::encode(&public_key[..16]))); Ok(Self { witness_did, @@ -183,7 +184,8 @@ impl WitnessServerState { let (seed, public_key) = provider_bridge::generate_ed25519_keypair_sync() .map_err(|e| WitnessError::Network(format!("failed to generate keypair: {}", e)))?; - let witness_did = DeviceDID::new_unchecked(format!("did:key:z6Mk{}", hex::encode(&public_key[..16]))); + let witness_did = + DeviceDID::new_unchecked(format!("did:key:z6Mk{}", hex::encode(&public_key[..16]))); let storage = WitnessStorage::in_memory()?; diff --git a/crates/auths-id/src/attestation/create.rs b/crates/auths-id/src/attestation/create.rs index 79d21940..537554e1 100644 --- a/crates/auths-id/src/attestation/create.rs +++ b/crates/auths-id/src/attestation/create.rs @@ -8,7 +8,7 @@ use auths_verifier::core::{ canonicalize_attestation_data, }; use auths_verifier::error::AttestationError; -use auths_verifier::types::DeviceDID; +use auths_verifier::types::{CanonicalDid, DeviceDID}; use chrono::{DateTime, Utc}; use log::debug; @@ -28,7 +28,7 @@ const MAX_CREATION_SKEW_SECS: i64 = 5 * 60; pub struct CanonicalRevocationData<'a> { pub version: u32, pub rid: &'a str, - pub issuer: &'a IdentityDID, + pub issuer: &'a CanonicalDid, pub subject: &'a DeviceDID, pub timestamp: &'a Option>, pub revoked_at: &'a Option>, // Should always be Some(...) @@ -91,10 +91,12 @@ pub fn create_signed_attestation( } // Construct the canonical data to be signed + let issuer_canonical = CanonicalDid::new_unchecked(identity_did.as_str()); + let delegated_canonical = delegated_by.as_ref().map(|d| CanonicalDid::from(d.clone())); let data_to_canonicalize = CanonicalAttestationData { version: ATTESTATION_VERSION, rid, - issuer: identity_did, + issuer: &issuer_canonical, subject: device_did, device_public_key, payload: &payload, @@ -109,7 +111,7 @@ pub fn create_signed_attestation( } else { Some(&capabilities) }, - delegated_by: delegated_by.as_ref(), + delegated_by: delegated_canonical.as_ref(), signer_type: None, }; @@ -158,7 +160,7 @@ pub fn create_signed_attestation( Ok(Attestation { version: ATTESTATION_VERSION, subject: device_did.clone(), - issuer: identity_did.clone(), + issuer: issuer_canonical, rid: ResourceId::new(rid), payload: payload.clone(), timestamp: meta.timestamp, @@ -171,7 +173,7 @@ pub fn create_signed_attestation( device_signature, role, capabilities, - delegated_by, + delegated_by: delegated_canonical, signer_type: None, environment_claim: None, }) diff --git a/crates/auths-id/src/attestation/revoke.rs b/crates/auths-id/src/attestation/revoke.rs index 130c7ae7..a754d4bd 100644 --- a/crates/auths-id/src/attestation/revoke.rs +++ b/crates/auths-id/src/attestation/revoke.rs @@ -3,7 +3,7 @@ use auths_core::signing::{PassphraseProvider, SecureSigner}; use auths_core::storage::keychain::{IdentityDID, KeyAlias}; use auths_verifier::core::{Attestation, Ed25519PublicKey, Ed25519Signature, ResourceId}; use auths_verifier::error::AttestationError; -use auths_verifier::types::DeviceDID; +use auths_verifier::types::{CanonicalDid, DeviceDID}; use chrono::{DateTime, Utc}; use log::{debug, warn}; @@ -45,10 +45,11 @@ pub fn create_signed_revocation( // 1. Construct the revocation-specific canonical data let revoked_at_value = Some(timestamp_arg); + let issuer_canonical = CanonicalDid::new_unchecked(identity_did.as_str()); let data_to_canonicalize_revocation = CanonicalRevocationData { version: REVOCATION_VERSION, rid, - issuer: identity_did, + issuer: &issuer_canonical, subject: device_did, timestamp: &Some(timestamp_arg), revoked_at: &revoked_at_value, @@ -83,7 +84,7 @@ pub fn create_signed_revocation( Ok(Attestation { version: REVOCATION_VERSION, subject: device_did.clone(), - issuer: identity_did.clone(), + issuer: CanonicalDid::new_unchecked(identity_did.as_str()), rid: ResourceId::new(rid), payload: payload_arg.clone(), timestamp: Some(timestamp_arg), diff --git a/crates/auths-id/src/attestation/verify.rs b/crates/auths-id/src/attestation/verify.rs index 59fee9f8..f259a07f 100644 --- a/crates/auths-id/src/attestation/verify.rs +++ b/crates/auths-id/src/attestation/verify.rs @@ -133,9 +133,8 @@ pub fn verify_with_resolver( mod tests { use super::*; use auths_core::signing::{DidResolverError, ResolvedDid}; - use auths_verifier::IdentityDID; use auths_verifier::core::{Ed25519PublicKey, Ed25519Signature, ResourceId}; - use auths_verifier::types::DeviceDID; + use auths_verifier::types::{CanonicalDid, DeviceDID}; struct StubResolver; impl DidResolver for StubResolver { @@ -148,7 +147,7 @@ mod tests { Attestation { version: 1, rid: ResourceId::new("test"), - issuer: IdentityDID::new_unchecked("did:keri:Estub"), + issuer: CanonicalDid::new_unchecked("did:keri:Estub"), subject: DeviceDID::new_unchecked("did:key:zDevice"), device_public_key: Ed25519PublicKey::from_bytes([0u8; 32]), identity_signature: Ed25519Signature::empty(), diff --git a/crates/auths-id/src/domain/attestation_message.rs b/crates/auths-id/src/domain/attestation_message.rs index 74aaa7a0..d40a2610 100644 --- a/crates/auths-id/src/domain/attestation_message.rs +++ b/crates/auths-id/src/domain/attestation_message.rs @@ -37,16 +37,15 @@ pub fn determine_commit_message( #[allow(clippy::disallowed_methods)] mod tests { use super::*; - use auths_core::storage::keychain::IdentityDID; use auths_verifier::core::{Ed25519PublicKey, Ed25519Signature, ResourceId}; - use auths_verifier::types::DeviceDID; + use auths_verifier::types::{CanonicalDid, DeviceDID}; use chrono::Utc; fn make_attestation(subject: &str, revoked: bool) -> Attestation { Attestation { version: 1, rid: ResourceId::new("test-rid"), - issuer: IdentityDID::new_unchecked("did:keri:EIssuer"), + issuer: CanonicalDid::new_unchecked("did:keri:EIssuer"), subject: DeviceDID::new_unchecked(subject), device_public_key: Ed25519PublicKey::from_bytes([0u8; 32]), identity_signature: Ed25519Signature::empty(), diff --git a/crates/auths-id/src/keri/cache.rs b/crates/auths-id/src/keri/cache.rs index c9e95763..aa61fd6b 100644 --- a/crates/auths-id/src/keri/cache.rs +++ b/crates/auths-id/src/keri/cache.rs @@ -12,8 +12,8 @@ //! - On any mismatch, cache is treated as a miss and full replay occurs //! - Cache files are local-only, never committed to Git or replicated -use auths_verifier::types::IdentityDID; use auths_verifier::CommitOid; +use auths_verifier::types::IdentityDID; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; diff --git a/crates/auths-id/src/policy/mod.rs b/crates/auths-id/src/policy/mod.rs index d83f4c40..189eaa3b 100644 --- a/crates/auths-id/src/policy/mod.rs +++ b/crates/auths-id/src/policy/mod.rs @@ -397,11 +397,10 @@ pub fn evaluate_with_receipts( #[allow(clippy::disallowed_methods)] mod tests { use super::*; - use auths_core::storage::keychain::IdentityDID; use auths_core::witness::NoOpWitness; use auths_verifier::core::{Capability, Ed25519PublicKey, Ed25519Signature, ResourceId}; use auths_verifier::keri::{Prefix, Said}; - use auths_verifier::types::DeviceDID; + use auths_verifier::types::{CanonicalDid, DeviceDID}; use chrono::Duration; /// Mock witness for testing @@ -441,7 +440,7 @@ mod tests { Attestation { version: 1, rid: ResourceId::new("test"), - issuer: IdentityDID::new_unchecked(issuer), + issuer: CanonicalDid::new_unchecked(issuer), subject: DeviceDID::new_unchecked("did:key:zSubject"), device_public_key: Ed25519PublicKey::from_bytes([0u8; 32]), identity_signature: Ed25519Signature::empty(), @@ -470,7 +469,7 @@ mod tests { let ctx = context_from_attestation(&att, now).unwrap(); assert_eq!(ctx.issuer.as_str(), "did:keri:ETest"); - assert_eq!(ctx.subject.as_str(), "did:key:subject"); + assert_eq!(ctx.subject.as_str(), "did:key:zSubject"); assert!(!ctx.revoked); } diff --git a/crates/auths-id/src/storage/indexed.rs b/crates/auths-id/src/storage/indexed.rs index e8b97ec6..4dc6526a 100644 --- a/crates/auths-id/src/storage/indexed.rs +++ b/crates/auths-id/src/storage/indexed.rs @@ -9,7 +9,7 @@ use crate::storage::attestation::AttestationSource; use crate::storage::layout::StorageLayoutConfig; use auths_index::{AttestationIndex, IndexedAttestation, rebuild_attestations_from_git}; use auths_verifier::core::{Attestation, CommitOid}; -use auths_verifier::types::DeviceDID; +use auths_verifier::types::{DeviceDID, IdentityDID}; use chrono::{DateTime, Utc}; use std::path::{Path, PathBuf}; @@ -88,7 +88,7 @@ impl IndexedAttestationStorage { ) -> Result<(), StorageError> { let indexed = IndexedAttestation { rid: att.rid.clone(), - issuer_did: att.issuer.clone(), + 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), @@ -151,10 +151,7 @@ impl AttestationSource for IndexedAttestationStorage { } // Collect unique device DIDs from the index - let mut dids: Vec = active - .into_iter() - .map(|a| a.device_did.clone()) - .collect(); + let mut dids: Vec = active.into_iter().map(|a| a.device_did.clone()).collect(); dids.sort_by(|a, b| a.as_str().cmp(b.as_str())); dids.dedup(); Ok(dids) diff --git a/crates/auths-id/src/storage/registry/backend.rs b/crates/auths-id/src/storage/registry/backend.rs index db909c4a..7cce66e9 100644 --- a/crates/auths-id/src/storage/registry/backend.rs +++ b/crates/auths-id/src/storage/registry/backend.rs @@ -551,7 +551,7 @@ pub trait RegistryBackend: Send + Sync { status, role: att.role, capabilities: att.capabilities.clone(), - issuer: att.issuer.clone(), + issuer: IdentityDID::new_unchecked(att.issuer.as_str()), rid: att.rid.clone(), revoked_at, expires_at: att.expires_at, diff --git a/crates/auths-id/src/storage/registry/org_member.rs b/crates/auths-id/src/storage/registry/org_member.rs index 476014eb..e04f1219 100644 --- a/crates/auths-id/src/storage/registry/org_member.rs +++ b/crates/auths-id/src/storage/registry/org_member.rs @@ -235,6 +235,7 @@ pub fn expected_org_issuer(org: &str) -> String { mod tests { use super::*; use auths_verifier::core::{Ed25519PublicKey, Ed25519Signature}; + use auths_verifier::types::CanonicalDid; #[test] fn member_filter_defaults_to_active_only() { @@ -275,7 +276,7 @@ mod tests { let att = Attestation { version: 1, rid: "test".into(), - issuer: IdentityDID::new_unchecked("did:keri:Eissuer"), + issuer: CanonicalDid::new_unchecked("did:keri:Eissuer"), subject: DeviceDID::new_unchecked("did:key:zSubject"), device_public_key: Ed25519PublicKey::from_bytes([0u8; 32]), identity_signature: Ed25519Signature::empty(), @@ -300,7 +301,7 @@ mod tests { let att = Attestation { version: 1, rid: "test".into(), - issuer: IdentityDID::new_unchecked("did:keri:Eissuer"), + issuer: CanonicalDid::new_unchecked("did:keri:Eissuer"), subject: DeviceDID::new_unchecked("did:key:zSubject"), device_public_key: Ed25519PublicKey::from_bytes([0u8; 32]), identity_signature: Ed25519Signature::empty(), @@ -326,7 +327,7 @@ mod tests { let att = Attestation { version: 1, rid: "test".into(), - issuer: IdentityDID::new_unchecked("did:keri:Eissuer"), + issuer: CanonicalDid::new_unchecked("did:keri:Eissuer"), subject: DeviceDID::new_unchecked("did:key:zSubject"), device_public_key: Ed25519PublicKey::from_bytes([0u8; 32]), identity_signature: Ed25519Signature::empty(), @@ -355,7 +356,7 @@ mod tests { let att = Attestation { version: 1, rid: "test".into(), - issuer: IdentityDID::new_unchecked("did:keri:Eissuer"), + issuer: CanonicalDid::new_unchecked("did:keri:Eissuer"), subject: DeviceDID::new_unchecked("did:key:zSubject"), device_public_key: Ed25519PublicKey::from_bytes([0u8; 32]), identity_signature: Ed25519Signature::empty(), @@ -381,7 +382,7 @@ mod tests { let att = Attestation { version: 1, rid: "test".into(), - issuer: IdentityDID::new_unchecked("did:keri:Eissuer"), + issuer: CanonicalDid::new_unchecked("did:keri:Eissuer"), subject: DeviceDID::new_unchecked("did:key:zSubject"), device_public_key: Ed25519PublicKey::from_bytes([0u8; 32]), identity_signature: Ed25519Signature::empty(), @@ -437,7 +438,7 @@ mod tests { let att = Attestation { version: 1, rid: "test".into(), - issuer: IdentityDID::new_unchecked("did:keri:Eissuer"), + issuer: CanonicalDid::new_unchecked("did:keri:Eissuer"), subject: DeviceDID::new_unchecked("did:key:zSubject"), device_public_key: Ed25519PublicKey::from_bytes([0u8; 32]), identity_signature: Ed25519Signature::empty(), diff --git a/crates/auths-id/src/testing/fakes/registry.rs b/crates/auths-id/src/testing/fakes/registry.rs index f5543393..8a563f7b 100644 --- a/crates/auths-id/src/testing/fakes/registry.rs +++ b/crates/auths-id/src/testing/fakes/registry.rs @@ -294,7 +294,7 @@ fn validate_org_member( if att.issuer.as_str() != expected_issuer { return Err(MemberInvalidReason::IssuerMismatch { expected_issuer: IdentityDID::new_unchecked(expected_issuer), - actual_issuer: att.issuer.clone(), + actual_issuer: IdentityDID::new_unchecked(att.issuer.as_str()), }); } if att.subject.as_str() != member_did_str { diff --git a/crates/auths-id/src/testing/fixtures.rs b/crates/auths-id/src/testing/fixtures.rs index 21ee33a7..26ada562 100644 --- a/crates/auths-id/src/testing/fixtures.rs +++ b/crates/auths-id/src/testing/fixtures.rs @@ -1,7 +1,6 @@ use auths_core::crypto::said::compute_next_commitment; -use auths_verifier::IdentityDID; use auths_verifier::core::{Attestation, Ed25519PublicKey, Ed25519Signature, ResourceId}; -use auths_verifier::types::DeviceDID; +use auths_verifier::types::{CanonicalDid, DeviceDID}; use base64::Engine; use base64::engine::general_purpose::URL_SAFE_NO_PAD; use ring::rand::SystemRandom; @@ -80,7 +79,7 @@ pub fn test_attestation(device_did: &DeviceDID, issuer: &str) -> Attestation { Attestation { version: 1, rid: ResourceId::new("test-rid"), - issuer: IdentityDID::new_unchecked(issuer), + issuer: CanonicalDid::new_unchecked(issuer), subject: device_did.clone(), device_public_key: Ed25519PublicKey::from_bytes([0u8; 32]), identity_signature: Ed25519Signature::empty(), diff --git a/crates/auths-index/src/index.rs b/crates/auths-index/src/index.rs index a68f62dc..7ec82d45 100644 --- a/crates/auths-index/src/index.rs +++ b/crates/auths-index/src/index.rs @@ -654,7 +654,7 @@ mod tests { let members = index.list_org_members_indexed("did:keri:EOrg").unwrap(); assert_eq!(members.len(), 1); - assert_eq!(members[0].member_did, "did:key:z6MkMember1"); + assert_eq!(members[0].member_did.as_str(), "did:key:z6MkMember1"); assert_eq!(members[0].rid, "rid-member-1"); } diff --git a/crates/auths-policy/Cargo.toml b/crates/auths-policy/Cargo.toml index ba444cdf..e2d26454 100644 --- a/crates/auths-policy/Cargo.toml +++ b/crates/auths-policy/Cargo.toml @@ -12,6 +12,7 @@ keywords = ["authorization", "policy", "permissions", "access-control"] categories = ["authentication"] [dependencies] +auths-verifier.workspace = true serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" chrono = { version = "0.4", features = ["serde"] } diff --git a/crates/auths-policy/src/compile.rs b/crates/auths-policy/src/compile.rs index 57aa11b8..5e5f465d 100644 --- a/crates/auths-policy/src/compile.rs +++ b/crates/auths-policy/src/compile.rs @@ -254,7 +254,7 @@ fn compile_inner( Err(e) => { errors.push(CompileError { path: path.into(), - message: e.0, + message: e.to_string(), }); CompiledExpr::False } @@ -269,7 +269,7 @@ fn compile_inner( Err(e) => { errors.push(CompileError { path: path.into(), - message: e.0, + message: e.to_string(), }); None } @@ -287,7 +287,7 @@ fn compile_inner( Err(e) => { errors.push(CompileError { path: path.into(), - message: e.0, + message: e.to_string(), }); None } @@ -301,7 +301,7 @@ fn compile_inner( Err(e) => { errors.push(CompileError { path: path.into(), - message: e.0, + message: e.to_string(), }); CompiledExpr::False } @@ -316,7 +316,7 @@ fn compile_inner( Err(e) => { errors.push(CompileError { path: path.into(), - message: e.0, + message: e.to_string(), }); None } @@ -330,7 +330,7 @@ fn compile_inner( Err(e) => { errors.push(CompileError { path: path.into(), - message: e.0, + message: e.to_string(), }); CompiledExpr::False } @@ -341,7 +341,7 @@ fn compile_inner( Err(e) => { errors.push(CompileError { path: path.into(), - message: e.0, + message: e.to_string(), }); CompiledExpr::False } @@ -352,7 +352,7 @@ fn compile_inner( Err(e) => { errors.push(CompileError { path: path.into(), - message: e.0, + message: e.to_string(), }); CompiledExpr::False } @@ -367,7 +367,7 @@ fn compile_inner( Err(e) => { errors.push(CompileError { path: path.into(), - message: e.0, + message: e.to_string(), }); None } @@ -398,7 +398,7 @@ fn compile_inner( Err(e) => { errors.push(CompileError { path: path.into(), - message: e.0, + message: e.to_string(), }); CompiledExpr::False } @@ -477,7 +477,7 @@ fn compile_inner( Err(e) => { errors.push(CompileError { path: path.into(), - message: e.0, + message: e.to_string(), }); None } diff --git a/crates/auths-policy/src/types.rs b/crates/auths-policy/src/types.rs index 416a534a..52e20314 100644 --- a/crates/auths-policy/src/types.rs +++ b/crates/auths-policy/src/types.rs @@ -7,133 +7,11 @@ use std::fmt; use serde::{Deserialize, Serialize}; -/// A validated, canonical DID. -/// -/// Constructed via `parse()` which enforces: -/// - Starts with `did:` -/// - Has at least method and id segments: `did:method:id` -/// - Lowercased method (KERI, key methods are case-sensitive in id, not method) -/// - No trailing whitespace or control characters -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] -#[serde(try_from = "String", into = "String")] -pub struct CanonicalDid(String); - -/// Error returned when parsing a DID fails. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct DidParseError(pub String); - -impl std::fmt::Display for DidParseError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0) - } -} - -impl std::error::Error for DidParseError {} - -impl CanonicalDid { - /// Parse and validate a DID string into canonical form. - /// - /// # Errors - /// - /// Returns an error if the DID is: - /// - Empty - /// - Missing required segments (did:method:id) - /// - Contains control characters - pub fn parse(raw: &str) -> Result { - // Check for control characters in the original input before trimming - // (newlines and other control chars would be stripped by trim) - if raw.chars().any(|c| c.is_control()) { - return Err(DidParseError("DID contains control characters".into())); - } - let trimmed = raw.trim(); - if trimmed.is_empty() { - return Err(DidParseError("empty DID".into())); - } - let parts: Vec<&str> = trimmed.splitn(3, ':').collect(); - if parts.len() < 3 || parts[0] != "did" || parts[1].is_empty() || parts[2].is_empty() { - return Err(DidParseError(format!("invalid DID format: '{}'", trimmed))); - } - // Canonical form: method segment lowercased, id preserved as-is - let canonical = format!("did:{}:{}", parts[1].to_lowercase(), parts[2]); - Ok(Self(canonical)) - } - - /// Returns the canonical DID as a string slice. - pub fn as_str(&self) -> &str { - &self.0 - } - - /// Returns the method-specific identifier (the part after `did:method:`). - /// - /// For `did:keri:EOrg1234567890` this returns `"EOrg1234567890"`. - /// - /// Usage: - /// ```ignore - /// let did = CanonicalDid::parse("did:keri:EOrg1234567890").unwrap(); - /// assert_eq!(did.method_specific_id(), "EOrg1234567890"); - /// ``` - pub fn method_specific_id(&self) -> &str { - // Always valid: parse() guarantees at least three colon-separated parts - self.0.splitn(3, ':').nth(2).unwrap_or("") - } +// CanonicalDid lives in auths-verifier (Layer 1) so all DID types are co-located. +pub use auths_verifier::types::CanonicalDid; - /// Validates that this DID uses the `keri` method with a valid KERI prefix. - /// - /// Enforces KERI derivation-code rules beyond generic DID validation: - /// - Method must be `keri` - /// - Method-specific ID must start with an ASCII uppercase letter - /// - Method-specific ID must be 2–128 characters long - /// - /// # Errors - /// - /// Returns `DidParseError` if any KERI constraint is violated. - /// - /// Usage: - /// ```ignore - /// let did = CanonicalDid::parse("did:keri:EOrg1234567890")?.require_keri()?; - /// ``` - pub fn require_keri(self) -> Result { - let parts: Vec<&str> = self.0.splitn(3, ':').collect(); - if parts[1] != "keri" { - return Err(DidParseError(format!( - "expected did:keri: DID, got did:{}:", - parts[1] - ))); - } - let id = parts[2]; - if id.len() < 2 || id.len() > 128 { - return Err(DidParseError( - "invalid KERI prefix: length must be 2–128 characters".into(), - )); - } - if !id.starts_with(|c: char| c.is_ascii_uppercase()) { - return Err(DidParseError(format!( - "invalid KERI prefix: must start with an uppercase derivation code, got '{}'", - &id[..1] - ))); - } - Ok(self) - } -} - -impl TryFrom for CanonicalDid { - type Error = DidParseError; - fn try_from(s: String) -> Result { - Self::parse(&s) - } -} - -impl From for String { - fn from(d: CanonicalDid) -> Self { - d.0 - } -} - -impl fmt::Display for CanonicalDid { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str(&self.0) - } -} +/// Re-export DidParseError from auths-verifier for backwards compatibility. +pub type DidParseError = auths_verifier::DidParseError; /// A validated capability identifier. /// diff --git a/crates/auths-radicle/src/attestation.rs b/crates/auths-radicle/src/attestation.rs index 076713ac..fa86c31e 100644 --- a/crates/auths-radicle/src/attestation.rs +++ b/crates/auths-radicle/src/attestation.rs @@ -11,8 +11,8 @@ //! to prevent the core from becoming a "God Object." The bridge converts //! between formats at the boundary via `TryFrom` impls. -use auths_verifier::IdentityDID; use auths_verifier::core::{Attestation, Ed25519PublicKey, Ed25519Signature, ResourceId}; +use auths_verifier::types::CanonicalDid; use auths_verifier::types::DeviceDID; use radicle_core::{Did, RepoId}; use radicle_crypto::PublicKey; @@ -227,7 +227,7 @@ impl TryFrom for Attestation { Ok(Attestation { version: 1, rid: ResourceId::new(rad.canonical_payload.rid.to_string()), - issuer: IdentityDID::new_unchecked(rad.canonical_payload.did.to_string()), + issuer: CanonicalDid::new_unchecked(rad.canonical_payload.did.to_string()), subject: DeviceDID::new_unchecked(rad.device_did.to_string()), device_public_key: Ed25519PublicKey::try_from_slice(rad.device_public_key.as_ref()) .map_err(|_e| { @@ -494,7 +494,7 @@ mod tests { let core = Attestation { version: 1, rid: ResourceId::new("rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5"), - issuer: IdentityDID::new_unchecked("did:keri:EXq5abc"), + issuer: CanonicalDid::new_unchecked("did:keri:EXq5abc"), subject: DeviceDID::new_unchecked(device_did.to_string()), device_public_key: Ed25519PublicKey::from_bytes(device_pk_bytes), identity_signature: Ed25519Signature::from_bytes([0xCD; 64]), diff --git a/crates/auths-radicle/src/verify.rs b/crates/auths-radicle/src/verify.rs index adf8cca0..c406486c 100644 --- a/crates/auths-radicle/src/verify.rs +++ b/crates/auths-radicle/src/verify.rs @@ -468,6 +468,7 @@ mod tests { use super::*; use auths_verifier::IdentityDID; use auths_verifier::keri::{Prefix, Said}; + use auths_verifier::types::CanonicalDid; use auths_verifier::types::DeviceDID as VerifierDeviceDID; use chrono::{DateTime, Utc}; use std::collections::HashMap; @@ -590,7 +591,7 @@ mod tests { Attestation { version: 1, rid: ResourceId::new("test"), - issuer: IdentityDID::new_unchecked(issuer.to_string()), + issuer: CanonicalDid::new_unchecked(issuer.to_string()), subject: DeviceDID::new_unchecked(device_did.to_string()), device_public_key: Ed25519PublicKey::from_bytes([0u8; 32]), identity_signature: Ed25519Signature::empty(), diff --git a/crates/auths-radicle/tests/cases/helpers.rs b/crates/auths-radicle/tests/cases/helpers.rs index 87cce41c..ebfafbb3 100644 --- a/crates/auths-radicle/tests/cases/helpers.rs +++ b/crates/auths-radicle/tests/cases/helpers.rs @@ -7,7 +7,7 @@ use auths_radicle::verify::AuthsStorage; use auths_verifier::IdentityDID; use auths_verifier::core::{Attestation, Capability, Ed25519PublicKey, Ed25519Signature}; use auths_verifier::keri::{Prefix, Said}; -use auths_verifier::types::DeviceDID; +use auths_verifier::types::{CanonicalDid, DeviceDID}; use radicle_core::{Did, RepoId}; use radicle_crypto::PublicKey; @@ -127,7 +127,7 @@ pub fn make_test_attestation( Attestation { version: 1, rid: auths_verifier::core::ResourceId::new(rid.to_string()), - issuer: IdentityDID::new_unchecked(issuer.to_string()), + issuer: CanonicalDid::new_unchecked(issuer.to_string()), subject: DeviceDID::new_unchecked(device_did.to_string()), device_public_key: Ed25519PublicKey::from_bytes([0u8; 32]), identity_signature: Ed25519Signature::empty(), diff --git a/crates/auths-sdk/src/workflows/org.rs b/crates/auths-sdk/src/workflows/org.rs index a1abcedc..c209e11c 100644 --- a/crates/auths-sdk/src/workflows/org.rs +++ b/crates/auths-sdk/src/workflows/org.rs @@ -345,10 +345,11 @@ pub fn add_organization_member( expires_at: None, }; + let admin_issuer_did = IdentityDID::new_unchecked(admin_att.issuer.as_str()); let attestation = create_signed_attestation( now, &rid, - &admin_att.issuer, + &admin_issuer_did, &member_did, cmd.member_public_key.as_bytes(), Some(serde_json::json!({ @@ -411,9 +412,10 @@ pub fn revoke_organization_member( let now = ctx.clock.now(); let member_did = DeviceDID::new_unchecked(&cmd.member_did); + let admin_issuer_did = IdentityDID::new_unchecked(admin_att.issuer.as_str()); let revocation = create_signed_revocation( admin_att.rid.as_str(), - &admin_att.issuer, + &admin_issuer_did, &member_did, cmd.member_public_key.as_bytes(), cmd.note, diff --git a/crates/auths-sdk/tests/cases/device.rs b/crates/auths-sdk/tests/cases/device.rs index b4110d49..cb234a06 100644 --- a/crates/auths-sdk/tests/cases/device.rs +++ b/crates/auths-sdk/tests/cases/device.rs @@ -98,7 +98,7 @@ fn extend_device_updates_expiry() { ); let config = DeviceExtensionConfig { repo_path: registry_path, - device_did: device_did.clone(), + device_did: auths_verifier::types::DeviceDID::new_unchecked(device_did.clone()), days: 365, identity_key_alias: key_alias.clone(), device_key_alias: Some(KeyAlias::new_unchecked("device-key")), @@ -133,7 +133,7 @@ fn extend_device_nonexistent_device_returns_error() { ); let config = DeviceExtensionConfig { repo_path: registry_path, - device_did: "did:key:zDoesNotExist".to_string(), + device_did: auths_verifier::types::DeviceDID::new_unchecked("did:key:zDoesNotExist"), days: 30, identity_key_alias: key_alias, device_key_alias: Some(KeyAlias::new_unchecked("device-key")), diff --git a/crates/auths-sdk/tests/cases/org.rs b/crates/auths-sdk/tests/cases/org.rs index ac800372..3e8703b9 100644 --- a/crates/auths-sdk/tests/cases/org.rs +++ b/crates/auths-sdk/tests/cases/org.rs @@ -15,7 +15,7 @@ use auths_verifier::PublicKeyHex; use auths_verifier::clock::ClockProvider; use auths_verifier::core::{Attestation, Ed25519PublicKey, Ed25519Signature, ResourceId}; use auths_verifier::testing::MockClock; -use auths_verifier::types::{DeviceDID, IdentityDID}; +use auths_verifier::types::{CanonicalDid, DeviceDID, IdentityDID}; use chrono::TimeZone; const ORG: &str = "ETestOrg0001"; @@ -42,7 +42,7 @@ fn base_admin_attestation() -> Attestation { Attestation { version: 1, rid: ResourceId::new("admin-rid-001"), - issuer: org_issuer(), + issuer: org_issuer().into(), subject: DeviceDID::new_unchecked(ADMIN_DID), device_public_key: Ed25519PublicKey::from_bytes(ADMIN_PUBKEY), identity_signature: Ed25519Signature::empty(), @@ -64,7 +64,7 @@ fn base_member_attestation() -> Attestation { Attestation { version: 1, rid: ResourceId::new("member-rid-001"), - issuer: org_issuer(), + issuer: org_issuer().into(), subject: DeviceDID::new_unchecked(MEMBER_DID), device_public_key: Ed25519PublicKey::from_bytes(MEMBER_PUBKEY), identity_signature: Ed25519Signature::empty(), @@ -76,7 +76,7 @@ fn base_member_attestation() -> Attestation { payload: None, role: Some(Role::Member), capabilities: vec![Capability::sign_commit()], - delegated_by: Some(IdentityDID::new_unchecked(ADMIN_DID)), + delegated_by: Some(CanonicalDid::new_unchecked(ADMIN_DID)), signer_type: None, environment_claim: None, } diff --git a/crates/auths-storage/src/git/adapter.rs b/crates/auths-storage/src/git/adapter.rs index a8b7ac77..a1774180 100644 --- a/crates/auths-storage/src/git/adapter.rs +++ b/crates/auths-storage/src/git/adapter.rs @@ -1209,8 +1209,8 @@ impl RegistryBackend for GitRegistryBackend { if let Some(index) = &self.index { let indexed = auths_index::IndexedAttestation { rid: attestation.rid.clone(), - issuer_did: attestation.issuer.to_string(), - device_did: attestation.subject.to_string(), + issuer_did: IdentityDID::new_unchecked(attestation.issuer.as_str()), + device_did: attestation.subject.clone(), git_ref: REGISTRY_REF.to_string(), commit_oid: auths_verifier::CommitOid::new_unchecked(""), revoked_at: attestation.revoked_at, @@ -1384,8 +1384,8 @@ impl RegistryBackend for GitRegistryBackend { if let Some(index) = &self.index { let indexed = auths_index::IndexedOrgMember { org_prefix: auths_verifier::keri::Prefix::new_unchecked(org.to_string()), - member_did: member.subject.to_string(), - issuer_did: member.issuer.to_string(), + member_did: member.subject.clone(), + issuer_did: IdentityDID::new_unchecked(member.issuer.as_str()), rid: member.rid.clone(), revoked_at: member.revoked_at, expires_at: member.expires_at, @@ -1455,7 +1455,7 @@ impl RegistryBackend for GitRegistryBackend { expected_issuer: IdentityDID::new_unchecked( expected_issuer.clone(), ), - actual_issuer: att.issuer.clone(), + actual_issuer: IdentityDID::new_unchecked(att.issuer.as_str()), }) } else { Ok(att) @@ -1570,7 +1570,7 @@ impl RegistryBackend for GitRegistryBackend { let members: Vec = indexed .into_iter() .map(|m| MemberView { - did: auths_verifier::types::DeviceDID::new_unchecked(&m.member_did), + did: m.member_did.clone(), status: MemberStatus::Active, role: None, capabilities: vec![], @@ -1791,8 +1791,8 @@ pub fn rebuild_org_members_from_registry( if let Ok(att) = &entry.attestation { let indexed = IndexedOrgMember { org_prefix: auths_verifier::keri::Prefix::new_unchecked(org_prefix.clone()), - member_did: entry.did.to_string(), - issuer_did: att.issuer.to_string(), + member_did: entry.did.clone(), + issuer_did: IdentityDID::new_unchecked(att.issuer.as_str()), rid: att.rid.clone(), revoked_at: att.revoked_at, expires_at: att.expires_at, @@ -2057,7 +2057,7 @@ mod tests { use auths_id::keri::types::{Prefix, Said}; use auths_id::keri::validate::{compute_event_said, finalize_icp_event, serialize_for_signing}; use auths_verifier::core::{Ed25519PublicKey, Ed25519Signature, ResourceId, Role}; - use auths_verifier::types::IdentityDID; + use auths_verifier::types::CanonicalDid; use base64::Engine; use base64::engine::general_purpose::URL_SAFE_NO_PAD; use chrono::{DateTime, Utc}; @@ -2426,7 +2426,7 @@ mod tests { let attestation = Attestation { version: 1, rid: ResourceId::new("test-rid"), - issuer: IdentityDID::new_unchecked("did:keri:EIssuer"), + issuer: CanonicalDid::new_unchecked("did:keri:EIssuer"), subject: did.clone(), device_public_key: Ed25519PublicKey::from_bytes([0u8; 32]), identity_signature: Ed25519Signature::empty(), @@ -2471,7 +2471,7 @@ mod tests { let original = Attestation { version: 1, rid: ResourceId::new("original"), - issuer: IdentityDID::new_unchecked("did:keri:EIssuer"), + issuer: CanonicalDid::new_unchecked("did:keri:EIssuer"), subject: did.clone(), device_public_key: Ed25519PublicKey::from_bytes([0u8; 32]), identity_signature: Ed25519Signature::empty(), @@ -2498,7 +2498,7 @@ mod tests { let updated = Attestation { version: 1, rid: ResourceId::new("updated"), - issuer: IdentityDID::new_unchecked("did:keri:EIssuer"), + issuer: CanonicalDid::new_unchecked("did:keri:EIssuer"), subject: did.clone(), device_public_key: Ed25519PublicKey::from_bytes([0u8; 32]), identity_signature: Ed25519Signature::empty(), @@ -2530,7 +2530,7 @@ mod tests { let att = Attestation { version: 1, rid: ResourceId::new("same-rid"), - issuer: IdentityDID::new_unchecked("did:keri:EIssuer"), + issuer: CanonicalDid::new_unchecked("did:keri:EIssuer"), subject: did.clone(), device_public_key: Ed25519PublicKey::from_bytes([0u8; 32]), identity_signature: Ed25519Signature::empty(), @@ -2565,7 +2565,7 @@ mod tests { let newer = Attestation { version: 1, rid: ResourceId::new("rid-newer"), - issuer: IdentityDID::new_unchecked("did:keri:EIssuer"), + issuer: CanonicalDid::new_unchecked("did:keri:EIssuer"), subject: did.clone(), device_public_key: Ed25519PublicKey::from_bytes([0u8; 32]), identity_signature: Ed25519Signature::empty(), @@ -2585,7 +2585,7 @@ mod tests { let older = Attestation { version: 1, rid: ResourceId::new("rid-older"), - issuer: IdentityDID::new_unchecked("did:keri:EIssuer"), + issuer: CanonicalDid::new_unchecked("did:keri:EIssuer"), subject: did.clone(), device_public_key: Ed25519PublicKey::from_bytes([0u8; 32]), identity_signature: Ed25519Signature::empty(), @@ -2615,7 +2615,7 @@ mod tests { let older = Attestation { version: 1, rid: ResourceId::new("rid-old"), - issuer: IdentityDID::new_unchecked("did:keri:EIssuer"), + issuer: CanonicalDid::new_unchecked("did:keri:EIssuer"), subject: did.clone(), device_public_key: Ed25519PublicKey::from_bytes([0u8; 32]), identity_signature: Ed25519Signature::empty(), @@ -2635,7 +2635,7 @@ mod tests { let newer = Attestation { version: 1, rid: ResourceId::new("rid-new"), - issuer: IdentityDID::new_unchecked("did:keri:EIssuer"), + issuer: CanonicalDid::new_unchecked("did:keri:EIssuer"), subject: did.clone(), device_public_key: Ed25519PublicKey::from_bytes([0u8; 32]), identity_signature: Ed25519Signature::empty(), @@ -2667,7 +2667,7 @@ mod tests { let revoked = Attestation { version: 1, rid: ResourceId::new("rid-revoked"), - issuer: IdentityDID::new_unchecked("did:keri:EIssuer"), + issuer: CanonicalDid::new_unchecked("did:keri:EIssuer"), subject: did.clone(), device_public_key: Ed25519PublicKey::from_bytes([0u8; 32]), identity_signature: Ed25519Signature::empty(), @@ -2687,7 +2687,7 @@ mod tests { let unrevoked_old = Attestation { version: 1, rid: ResourceId::new("rid-unrevoked"), - issuer: IdentityDID::new_unchecked("did:keri:EIssuer"), + issuer: CanonicalDid::new_unchecked("did:keri:EIssuer"), subject: did.clone(), device_public_key: Ed25519PublicKey::from_bytes([0u8; 32]), identity_signature: Ed25519Signature::empty(), @@ -2717,7 +2717,7 @@ mod tests { let att = Attestation { version: 1, rid: ResourceId::new("first-ever"), - issuer: IdentityDID::new_unchecked("did:keri:EIssuer"), + issuer: CanonicalDid::new_unchecked("did:keri:EIssuer"), subject: did.clone(), device_public_key: Ed25519PublicKey::from_bytes([0u8; 32]), identity_signature: Ed25519Signature::empty(), @@ -2745,7 +2745,7 @@ mod tests { let with_ts = Attestation { version: 1, rid: ResourceId::new("rid-with-ts"), - issuer: IdentityDID::new_unchecked("did:keri:EIssuer"), + issuer: CanonicalDid::new_unchecked("did:keri:EIssuer"), subject: did.clone(), device_public_key: Ed25519PublicKey::from_bytes([0u8; 32]), identity_signature: Ed25519Signature::empty(), @@ -2765,7 +2765,7 @@ mod tests { let without_ts = Attestation { version: 1, rid: ResourceId::new("rid-no-ts"), - issuer: IdentityDID::new_unchecked("did:keri:EIssuer"), + issuer: CanonicalDid::new_unchecked("did:keri:EIssuer"), subject: did.clone(), device_public_key: Ed25519PublicKey::from_bytes([0u8; 32]), identity_signature: Ed25519Signature::empty(), @@ -2797,7 +2797,7 @@ mod tests { let att1 = Attestation { version: 1, rid: ResourceId::new("rid1"), - issuer: IdentityDID::new_unchecked("did:keri:EIssuer"), + issuer: CanonicalDid::new_unchecked("did:keri:EIssuer"), subject: did1.clone(), device_public_key: Ed25519PublicKey::from_bytes([0u8; 32]), identity_signature: Ed25519Signature::empty(), @@ -2817,7 +2817,7 @@ mod tests { let att2 = Attestation { version: 1, rid: ResourceId::new("rid2"), - issuer: IdentityDID::new_unchecked("did:keri:EIssuer"), + issuer: CanonicalDid::new_unchecked("did:keri:EIssuer"), subject: did2.clone(), device_public_key: Ed25519PublicKey::from_bytes([1u8; 32]), identity_signature: Ed25519Signature::empty(), @@ -2866,7 +2866,7 @@ mod tests { .store_attestation(&Attestation { version: 1, rid: ResourceId::new("rid"), - issuer: IdentityDID::new_unchecked("did:keri:EIssuer"), + issuer: CanonicalDid::new_unchecked("did:keri:EIssuer"), subject: did, device_public_key: Ed25519PublicKey::from_bytes([0u8; 32]), identity_signature: Ed25519Signature::empty(), @@ -2899,7 +2899,7 @@ mod tests { let member_att = Attestation { version: 1, rid: ResourceId::new("org-member"), - issuer: IdentityDID::new_unchecked(format!("did:keri:{}", org)), + issuer: CanonicalDid::new_unchecked(format!("did:keri:{}", org)), subject: member_did.clone(), device_public_key: Ed25519PublicKey::from_bytes([0u8; 32]), identity_signature: Ed25519Signature::empty(), @@ -2985,7 +2985,7 @@ mod tests { let att = Attestation { version: 1, rid: ResourceId::new("mismatch-test"), - issuer: IdentityDID::new_unchecked(format!("did:keri:{}", org)), + issuer: CanonicalDid::new_unchecked(format!("did:keri:{}", org)), subject: correct_did, device_public_key: Ed25519PublicKey::from_bytes([0u8; 32]), identity_signature: Ed25519Signature::empty(), @@ -3049,7 +3049,7 @@ mod tests { let att = Attestation { version: 1, rid: ResourceId::new("issuer-mismatch-test"), - issuer: IdentityDID::new_unchecked("did:keri:EDifferentOrg"), // WRONG issuer + issuer: CanonicalDid::new_unchecked("did:keri:EDifferentOrg"), // WRONG issuer subject: member_did.clone(), device_public_key: Ed25519PublicKey::from_bytes([0u8; 32]), identity_signature: Ed25519Signature::empty(), @@ -3100,7 +3100,7 @@ mod tests { let active_att = Attestation { version: 1, rid: ResourceId::new("active"), - issuer: IdentityDID::new_unchecked(format!("did:keri:{}", org)), + issuer: CanonicalDid::new_unchecked(format!("did:keri:{}", org)), subject: active_did.clone(), device_public_key: Ed25519PublicKey::from_bytes([0u8; 32]), identity_signature: Ed25519Signature::empty(), @@ -3123,7 +3123,7 @@ mod tests { let revoked_att = Attestation { version: 1, rid: ResourceId::new("revoked"), - issuer: IdentityDID::new_unchecked(format!("did:keri:{}", org)), + issuer: CanonicalDid::new_unchecked(format!("did:keri:{}", org)), subject: revoked_did.clone(), device_public_key: Ed25519PublicKey::from_bytes([0u8; 32]), identity_signature: Ed25519Signature::empty(), @@ -3169,7 +3169,7 @@ mod tests { let revoked_att = Attestation { version: 1, rid: ResourceId::new("revoked"), - issuer: IdentityDID::new_unchecked(format!("did:keri:{}", org)), + issuer: CanonicalDid::new_unchecked(format!("did:keri:{}", org)), subject: revoked_did.clone(), device_public_key: Ed25519PublicKey::from_bytes([0u8; 32]), identity_signature: Ed25519Signature::empty(), @@ -3211,7 +3211,7 @@ mod tests { let expired_att = Attestation { version: 1, rid: ResourceId::new("expired"), - issuer: IdentityDID::new_unchecked(format!("did:keri:{}", org)), + issuer: CanonicalDid::new_unchecked(format!("did:keri:{}", org)), subject: expired_did.clone(), device_public_key: Ed25519PublicKey::from_bytes([0u8; 32]), identity_signature: Ed25519Signature::empty(), @@ -3256,7 +3256,7 @@ mod tests { let org_att = Attestation { version: 1, rid: ResourceId::new("org"), - issuer: IdentityDID::new_unchecked(org_issuer.clone()), + issuer: CanonicalDid::new_unchecked(org_issuer.clone()), subject: org_member_did.clone(), device_public_key: Ed25519PublicKey::from_bytes([0u8; 32]), identity_signature: Ed25519Signature::empty(), @@ -3279,7 +3279,7 @@ mod tests { let wrong_att = Attestation { version: 1, rid: ResourceId::new("wrong"), - issuer: IdentityDID::new_unchecked("did:keri:EDifferentIssuer"), // WRONG! + issuer: CanonicalDid::new_unchecked("did:keri:EDifferentIssuer"), // WRONG! subject: wrong_did.clone(), device_public_key: Ed25519PublicKey::from_bytes([0u8; 32]), identity_signature: Ed25519Signature::empty(), @@ -3337,7 +3337,7 @@ mod tests { let admin_att = Attestation { version: 1, rid: ResourceId::new("admin"), - issuer: IdentityDID::new_unchecked(format!("did:keri:{}", org)), + issuer: CanonicalDid::new_unchecked(format!("did:keri:{}", org)), subject: admin_did.clone(), device_public_key: Ed25519PublicKey::from_bytes([0u8; 32]), identity_signature: Ed25519Signature::empty(), @@ -3360,7 +3360,7 @@ mod tests { let member_att = Attestation { version: 1, rid: ResourceId::new("member"), - issuer: IdentityDID::new_unchecked(format!("did:keri:{}", org)), + issuer: CanonicalDid::new_unchecked(format!("did:keri:{}", org)), subject: member_did, device_public_key: Ed25519PublicKey::from_bytes([0u8; 32]), identity_signature: Ed25519Signature::empty(), @@ -3405,7 +3405,7 @@ mod tests { let signer_att = Attestation { version: 1, rid: ResourceId::new("signer"), - issuer: IdentityDID::new_unchecked(format!("did:keri:{}", org)), + issuer: CanonicalDid::new_unchecked(format!("did:keri:{}", org)), subject: signer_did.clone(), device_public_key: Ed25519PublicKey::from_bytes([0u8; 32]), identity_signature: Ed25519Signature::empty(), @@ -3428,7 +3428,7 @@ mod tests { let nocap_att = Attestation { version: 1, rid: ResourceId::new("nocap"), - issuer: IdentityDID::new_unchecked(format!("did:keri:{}", org)), + issuer: CanonicalDid::new_unchecked(format!("did:keri:{}", org)), subject: nocap_did, device_public_key: Ed25519PublicKey::from_bytes([0u8; 32]), identity_signature: Ed25519Signature::empty(), @@ -3473,7 +3473,7 @@ mod tests { let both_att = Attestation { version: 1, rid: ResourceId::new("both"), - issuer: IdentityDID::new_unchecked(format!("did:keri:{}", org)), + issuer: CanonicalDid::new_unchecked(format!("did:keri:{}", org)), subject: both_did.clone(), device_public_key: Ed25519PublicKey::from_bytes([0u8; 32]), identity_signature: Ed25519Signature::empty(), @@ -3496,7 +3496,7 @@ mod tests { let one_att = Attestation { version: 1, rid: ResourceId::new("one"), - issuer: IdentityDID::new_unchecked(format!("did:keri:{}", org)), + issuer: CanonicalDid::new_unchecked(format!("did:keri:{}", org)), subject: one_did, device_public_key: Ed25519PublicKey::from_bytes([0u8; 32]), identity_signature: Ed25519Signature::empty(), @@ -3540,7 +3540,7 @@ mod tests { let valid_att = Attestation { version: 1, rid: ResourceId::new("valid"), - issuer: IdentityDID::new_unchecked(format!("did:keri:{}", org)), + issuer: CanonicalDid::new_unchecked(format!("did:keri:{}", org)), subject: valid_did.clone(), device_public_key: Ed25519PublicKey::from_bytes([0u8; 32]), identity_signature: Ed25519Signature::empty(), @@ -3626,7 +3626,7 @@ mod tests { let att = Attestation { version: 1, rid: ResourceId::new("test"), - issuer: IdentityDID::new_unchecked(format!("did:keri:{}", org)), + issuer: CanonicalDid::new_unchecked(format!("did:keri:{}", org)), subject: did, device_public_key: Ed25519PublicKey::from_bytes([0u8; 32]), identity_signature: Ed25519Signature::empty(), @@ -3676,7 +3676,7 @@ mod tests { let attestation = Attestation { version: 1, rid: ResourceId::new("source-test"), - issuer: IdentityDID::new_unchecked("did:keri:EIssuer"), + issuer: CanonicalDid::new_unchecked("did:keri:EIssuer"), subject: did.clone(), device_public_key: Ed25519PublicKey::from_bytes([0u8; 32]), identity_signature: Ed25519Signature::empty(), @@ -3720,7 +3720,7 @@ mod tests { let attestation = Attestation { version: 1, rid: ResourceId::new(format!("rid-{}", i)), - issuer: IdentityDID::new_unchecked("did:keri:EIssuer"), + issuer: CanonicalDid::new_unchecked("did:keri:EIssuer"), subject: did, device_public_key: Ed25519PublicKey::from_bytes([i as u8; 32]), identity_signature: Ed25519Signature::empty(), @@ -3756,7 +3756,7 @@ mod tests { let attestation = Attestation { version: 1, rid: ResourceId::new("discover-test"), - issuer: IdentityDID::new_unchecked("did:keri:EIssuer"), + issuer: CanonicalDid::new_unchecked("did:keri:EIssuer"), subject: did.clone(), device_public_key: Ed25519PublicKey::from_bytes([0u8; 32]), identity_signature: Ed25519Signature::empty(), @@ -3796,7 +3796,7 @@ mod tests { let attestation = Attestation { version: 1, rid: ResourceId::new("sink-test"), - issuer: IdentityDID::new_unchecked("did:keri:EIssuer"), + issuer: CanonicalDid::new_unchecked("did:keri:EIssuer"), subject: did.clone(), device_public_key: Ed25519PublicKey::from_bytes([0u8; 32]), identity_signature: Ed25519Signature::empty(), @@ -3834,7 +3834,7 @@ mod tests { let attestation1 = Attestation { version: 1, rid: ResourceId::new("original"), - issuer: IdentityDID::new_unchecked("did:keri:EIssuer"), + issuer: CanonicalDid::new_unchecked("did:keri:EIssuer"), subject: did.clone(), device_public_key: Ed25519PublicKey::from_bytes([0u8; 32]), identity_signature: Ed25519Signature::empty(), @@ -3858,7 +3858,7 @@ mod tests { let attestation2 = Attestation { version: 1, rid: ResourceId::new("updated"), - issuer: IdentityDID::new_unchecked("did:keri:EIssuer"), + issuer: CanonicalDid::new_unchecked("did:keri:EIssuer"), subject: did.clone(), device_public_key: Ed25519PublicKey::from_bytes([0u8; 32]), identity_signature: Ed25519Signature::empty(), @@ -4062,7 +4062,7 @@ mod index_consistency_tests { use auths_id::keri::validate::{finalize_icp_event, serialize_for_signing}; use auths_id::storage::registry::org_member::MemberFilter; use auths_verifier::core::{Ed25519PublicKey, Ed25519Signature, ResourceId}; - use auths_verifier::types::{DeviceDID, IdentityDID}; + use auths_verifier::types::{CanonicalDid, DeviceDID}; use base64::Engine; use base64::engine::general_purpose::URL_SAFE_NO_PAD; use chrono::Utc; @@ -4119,7 +4119,7 @@ mod index_consistency_tests { Attestation { version: 1, rid: ResourceId::new(rid), - issuer: IdentityDID::new_unchecked(format!("did:keri:{}", org_prefix)), + issuer: CanonicalDid::new_unchecked(format!("did:keri:{}", org_prefix)), subject: DeviceDID::new_unchecked(format!("did:key:z6Mk{}", did_suffix)), device_public_key: Ed25519PublicKey::from_bytes([0u8; 32]), identity_signature: Ed25519Signature::empty(), @@ -4298,7 +4298,7 @@ mod tenant_isolation_tests { use std::sync::Arc; use auths_verifier::core::{Attestation, Ed25519PublicKey, Ed25519Signature, ResourceId}; - use auths_verifier::types::{DeviceDID, IdentityDID}; + use auths_verifier::types::{CanonicalDid, DeviceDID}; use base64::Engine; use base64::engine::general_purpose::URL_SAFE_NO_PAD; use ring::rand::SystemRandom; @@ -4368,7 +4368,7 @@ mod tenant_isolation_tests { Attestation { version: 1, rid: ResourceId::new("test-rid"), - issuer: IdentityDID::new_unchecked("did:keri:EIssuer"), + issuer: CanonicalDid::new_unchecked("did:keri:EIssuer"), subject: did, device_public_key: Ed25519PublicKey::from_bytes([0u8; 32]), identity_signature: Ed25519Signature::empty(), diff --git a/crates/auths-storage/src/git/attestation_adapter.rs b/crates/auths-storage/src/git/attestation_adapter.rs index 67d8ae77..ce9f800e 100644 --- a/crates/auths-storage/src/git/attestation_adapter.rs +++ b/crates/auths-storage/src/git/attestation_adapter.rs @@ -32,6 +32,8 @@ use std::path::PathBuf; use auths_id::error::StorageError; use auths_verifier::core::{Attestation, VerifiedAttestation}; use auths_verifier::types::DeviceDID; +#[cfg(feature = "indexed-storage")] +use auths_verifier::types::IdentityDID; use auths_id::attestation::AttestationSink; use auths_id::storage::attestation::AttestationSource; @@ -213,8 +215,8 @@ impl AttestationSink for RegistryAttestationStorage { // Timestamp fallback for missing attestation timestamp let indexed = IndexedAttestation { rid: attestation.rid.clone(), - issuer_did: attestation.issuer.to_string(), - device_did: attestation.subject.to_string(), + issuer_did: IdentityDID::new_unchecked(attestation.issuer.as_str()), + device_did: attestation.subject.clone(), git_ref, commit_oid: auths_verifier::CommitOid::new_unchecked(""), revoked_at: attestation.revoked_at, @@ -238,7 +240,7 @@ impl AttestationSink for RegistryAttestationStorage { mod tests { use super::*; use auths_verifier::core::{Ed25519PublicKey, Ed25519Signature, ResourceId}; - use auths_verifier::types::IdentityDID; + use auths_verifier::types::CanonicalDid; use git2::Repository; use tempfile::TempDir; @@ -260,7 +262,7 @@ mod tests { Attestation { version: 1, rid: ResourceId::new(format!("test-rid-{}", seq)), - issuer: IdentityDID::new_unchecked("did:keri:ETestIssuer"), + issuer: CanonicalDid::new_unchecked("did:keri:ETestIssuer"), subject: DeviceDID::new_unchecked(subject), device_public_key: Ed25519PublicKey::from_bytes([0u8; 32]), identity_signature: Ed25519Signature::empty(), diff --git a/crates/auths-verifier/src/core.rs b/crates/auths-verifier/src/core.rs index 6cd6bb06..b6bf5d9b 100644 --- a/crates/auths-verifier/src/core.rs +++ b/crates/auths-verifier/src/core.rs @@ -1,7 +1,7 @@ //! Core attestation types and canonical serialization. use crate::error::AttestationError; -use crate::types::{DeviceDID, IdentityDID}; +use crate::types::{CanonicalDid, DeviceDID, IdentityDID}; use chrono::{DateTime, Utc}; use hex; use json_canon; @@ -703,8 +703,8 @@ pub struct Attestation { pub version: u32, /// Record identifier linking this attestation to its storage ref. pub rid: ResourceId, - /// DID of the issuing identity. - pub issuer: IdentityDID, + /// DID of the issuing identity (can be `did:keri:` or `did:key:`). + pub issuer: CanonicalDid, /// DID of the device being attested. pub subject: DeviceDID, /// Ed25519 public key of the device (32 bytes, hex-encoded in JSON). @@ -739,7 +739,7 @@ pub struct Attestation { /// DID of the attestation that delegated authority (for chain tracking). #[serde(default, skip_serializing_if = "Option::is_none")] - pub delegated_by: Option, + pub delegated_by: Option, /// The type of entity that produced this signature (human, agent, workload). /// Included in the canonical JSON before signing — the signature covers this field. @@ -822,7 +822,7 @@ pub struct CanonicalAttestationData<'a> { /// Record identifier. pub rid: &'a str, /// DID of the issuing identity. - pub issuer: &'a IdentityDID, + pub issuer: &'a CanonicalDid, /// DID of the device being attested. pub subject: &'a DeviceDID, /// Raw Ed25519 public key of the device. @@ -847,7 +847,7 @@ pub struct CanonicalAttestationData<'a> { pub capabilities: Option<&'a Vec>, /// DID of the delegating attestation (included in signed envelope). #[serde(skip_serializing_if = "Option::is_none")] - pub delegated_by: Option<&'a IdentityDID>, + pub delegated_by: Option<&'a CanonicalDid>, /// Type of signer (included in signed envelope). #[serde(skip_serializing_if = "Option::is_none")] pub signer_type: Option<&'a SignerType>, @@ -1617,7 +1617,7 @@ mod tests { let att = Attestation { version: 1, rid: ResourceId::new("test-rid"), - issuer: IdentityDID::new_unchecked("did:keri:Eissuer"), + issuer: CanonicalDid::new_unchecked("did:keri:Eissuer"), subject: DeviceDID::new_unchecked("did:key:zSubject"), device_public_key: Ed25519PublicKey::from_bytes([0u8; 32]), identity_signature: Ed25519Signature::empty(), @@ -1629,7 +1629,7 @@ mod tests { payload: None, role: Some(Role::Admin), capabilities: vec![Capability::sign_commit(), Capability::manage_members()], - delegated_by: Some(IdentityDID::new_unchecked("did:keri:Edelegator")), + delegated_by: Some(CanonicalDid::new_unchecked("did:keri:Edelegator")), signer_type: None, environment_claim: None, }; @@ -1650,7 +1650,7 @@ mod tests { let att = Attestation { version: 1, rid: ResourceId::new("test-rid"), - issuer: IdentityDID::new_unchecked("did:keri:Eissuer"), + issuer: CanonicalDid::new_unchecked("did:keri:Eissuer"), subject: DeviceDID::new_unchecked("did:key:zSubject"), device_public_key: Ed25519PublicKey::from_bytes([0u8; 32]), identity_signature: Ed25519Signature::empty(), @@ -1683,7 +1683,7 @@ mod tests { let original = Attestation { version: 1, rid: ResourceId::new("test-rid"), - issuer: IdentityDID::new_unchecked("did:keri:Eissuer"), + issuer: CanonicalDid::new_unchecked("did:keri:Eissuer"), subject: DeviceDID::new_unchecked("did:key:zSubject"), device_public_key: Ed25519PublicKey::from_bytes([0u8; 32]), identity_signature: Ed25519Signature::empty(), @@ -1695,7 +1695,7 @@ mod tests { payload: None, role: Some(Role::Member), capabilities: vec![Capability::sign_commit(), Capability::sign_release()], - delegated_by: Some(IdentityDID::new_unchecked("did:keri:Eadmin")), + delegated_by: Some(CanonicalDid::new_unchecked("did:keri:Eadmin")), signer_type: None, environment_claim: None, }; @@ -1873,7 +1873,7 @@ mod tests { let attestation = Attestation { version: 1, rid: ResourceId::new("test-rid"), - issuer: IdentityDID::new_unchecked("did:keri:Eissuer"), + issuer: CanonicalDid::new_unchecked("did:keri:Eissuer"), subject: DeviceDID::new_unchecked("did:key:zSubject"), device_public_key: Ed25519PublicKey::from_bytes([0u8; 32]), identity_signature: Ed25519Signature::empty(), diff --git a/crates/auths-verifier/src/lib.rs b/crates/auths-verifier/src/lib.rs index b47696ae..33c8c325 100644 --- a/crates/auths-verifier/src/lib.rs +++ b/crates/auths-verifier/src/lib.rs @@ -69,8 +69,8 @@ pub mod witness; // Re-export verification types for convenience pub use types::{ - ChainLink, DeviceDID, DidConversionError, DidParseError, IdentityDID, VerificationReport, - VerificationStatus, signer_hex_to_did, validate_did, + CanonicalDid, ChainLink, DeviceDID, DidConversionError, DidParseError, IdentityDID, + VerificationReport, VerificationStatus, signer_hex_to_did, validate_did, }; // Re-export action envelope diff --git a/crates/auths-verifier/src/types.rs b/crates/auths-verifier/src/types.rs index a56838fb..a7b62a6e 100644 --- a/crates/auths-verifier/src/types.rs +++ b/crates/auths-verifier/src/types.rs @@ -490,6 +490,176 @@ pub enum DidParseError { /// The method-specific identifier portion is empty. #[error("DID method-specific identifier is empty")] EmptyIdentifier, + /// Generic DID format validation failure (used by `CanonicalDid`). + #[error("{0}")] + InvalidFormat(String), + /// DID string contains control characters. + #[error("DID contains control characters")] + ControlCharacters, +} + +// ============================================================================ +// CanonicalDid Type +// ============================================================================ + +/// A validated, canonical DID that accepts any method (`did:keri:`, `did:key:`, etc.). +/// +/// Use this for fields that can hold either identity or device DIDs, +/// such as attestation issuers which may be `did:keri:` or `did:key:`. +/// +/// Constructed via `parse()` which enforces: +/// - Starts with `did:` +/// - Has at least method and id segments: `did:method:id` +/// - Lowercased method (KERI, key methods are case-sensitive in id, not method) +/// - No trailing whitespace or control characters +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] +#[serde(transparent)] +pub struct CanonicalDid(String); + +impl CanonicalDid { + /// Parse and validate a DID string into canonical form. + pub fn parse(raw: &str) -> Result { + if raw.chars().any(|c| c.is_control()) { + return Err(DidParseError::ControlCharacters); + } + let trimmed = raw.trim(); + if trimmed.is_empty() { + return Err(DidParseError::EmptyIdentifier); + } + let parts: Vec<&str> = trimmed.splitn(3, ':').collect(); + if parts.len() < 3 || parts[0] != "did" || parts[1].is_empty() || parts[2].is_empty() { + return Err(DidParseError::InvalidFormat(format!( + "invalid DID format: '{}'", + trimmed + ))); + } + let canonical = format!("did:{}:{}", parts[1].to_lowercase(), parts[2]); + Ok(Self(canonical)) + } + + /// Wraps a DID string without validation (for trusted internal paths). + pub fn new_unchecked>(s: S) -> Self { + Self(s.into()) + } + + /// Returns the canonical DID as a string slice. + pub fn as_str(&self) -> &str { + &self.0 + } + + /// Returns the method-specific identifier (the part after `did:method:`). + pub fn method_specific_id(&self) -> &str { + self.0.splitn(3, ':').nth(2).unwrap_or("") + } + + /// Validates that this DID uses the `keri` method with a valid KERI prefix. + pub fn require_keri(self) -> Result { + let parts: Vec<&str> = self.0.splitn(3, ':').collect(); + if parts[1] != "keri" { + return Err(DidParseError::InvalidFormat(format!( + "expected did:keri: DID, got did:{}:", + parts[1] + ))); + } + let id = parts[2]; + if id.len() < 2 || id.len() > 128 { + return Err(DidParseError::InvalidFormat( + "invalid KERI prefix: length must be 2–128 characters".into(), + )); + } + if !id.starts_with(|c: char| c.is_ascii_uppercase()) { + return Err(DidParseError::InvalidFormat(format!( + "invalid KERI prefix: must start with an uppercase derivation code, got '{}'", + &id[..1] + ))); + } + Ok(self) + } + + /// Consumes self and returns the inner String. + pub fn into_inner(self) -> String { + self.0 + } +} + +impl TryFrom for CanonicalDid { + type Error = DidParseError; + fn try_from(s: String) -> Result { + Self::parse(&s) + } +} + +impl TryFrom<&str> for CanonicalDid { + type Error = DidParseError; + fn try_from(s: &str) -> Result { + Self::parse(s) + } +} + +impl From for String { + fn from(d: CanonicalDid) -> Self { + d.0 + } +} + +impl fmt::Display for CanonicalDid { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(&self.0) + } +} + +impl Deref for CanonicalDid { + type Target = str; + fn deref(&self) -> &str { + &self.0 + } +} + +impl AsRef for CanonicalDid { + fn as_ref(&self) -> &str { + &self.0 + } +} + +impl Borrow for CanonicalDid { + fn borrow(&self) -> &str { + &self.0 + } +} + +impl<'de> serde::Deserialize<'de> for CanonicalDid { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + Self::parse(&s).map_err(serde::de::Error::custom) + } +} + +impl PartialEq for CanonicalDid { + fn eq(&self, other: &str) -> bool { + self.0 == other + } +} + +impl PartialEq<&str> for CanonicalDid { + fn eq(&self, other: &&str) -> bool { + self.0 == *other + } +} + +impl From for CanonicalDid { + fn from(did: IdentityDID) -> Self { + Self(did.into_inner()) + } +} + +impl From for CanonicalDid { + fn from(did: DeviceDID) -> Self { + Self(did.0) + } } #[cfg(test)] diff --git a/crates/auths-verifier/src/verify.rs b/crates/auths-verifier/src/verify.rs index 259fac1e..8680a36f 100644 --- a/crates/auths-verifier/src/verify.rs +++ b/crates/auths-verifier/src/verify.rs @@ -565,7 +565,7 @@ mod tests { use crate::clock::ClockProvider; use crate::core::{Capability, Ed25519PublicKey, Ed25519Signature, ResourceId, Role}; use crate::keri::Said; - use crate::types::{DeviceDID, IdentityDID}; + use crate::types::{CanonicalDid, DeviceDID}; use crate::verifier::Verifier; use auths_crypto::RingCryptoProvider; use auths_crypto::testing::create_test_keypair; @@ -605,7 +605,7 @@ mod tests { let mut att = Attestation { version: 1, rid: ResourceId::new("test-rid"), - issuer: IdentityDID::new_unchecked(issuer_did), + issuer: CanonicalDid::new_unchecked(issuer_did), subject: DeviceDID::new_unchecked(subject_did), device_public_key: Ed25519PublicKey::from_bytes(device_pk), identity_signature: Ed25519Signature::empty(), @@ -1213,7 +1213,7 @@ mod tests { let mut att = Attestation { version: 1, rid: ResourceId::new("test-rid"), - issuer: IdentityDID::new_unchecked(issuer_did), + issuer: CanonicalDid::new_unchecked(issuer_did), subject: DeviceDID::new_unchecked(subject_did), device_public_key: Ed25519PublicKey::from_bytes(device_pk), identity_signature: Ed25519Signature::empty(), @@ -1519,7 +1519,7 @@ mod tests { .is_ok() ); - att.delegated_by = Some(IdentityDID::new_unchecked("did:keri:Eattacker")); + att.delegated_by = Some(CanonicalDid::new_unchecked("did:keri:Eattacker")); let result = test_verifier().verify_with_keys(&att, &root_pk).await; assert!( result.is_err(), @@ -1563,7 +1563,7 @@ mod tests { let mut att = Attestation { version: 1, rid: ResourceId::new("test-rid"), - issuer: IdentityDID::new_unchecked(issuer_did), + issuer: CanonicalDid::new_unchecked(issuer_did), subject: DeviceDID::new_unchecked(subject_did), device_public_key: Ed25519PublicKey::from_bytes(device_pk), identity_signature: Ed25519Signature::empty(), diff --git a/crates/auths-verifier/tests/cases/expiration_skew.rs b/crates/auths-verifier/tests/cases/expiration_skew.rs index e5d2a0bf..d0db1283 100644 --- a/crates/auths-verifier/tests/cases/expiration_skew.rs +++ b/crates/auths-verifier/tests/cases/expiration_skew.rs @@ -3,7 +3,7 @@ use auths_verifier::core::{ Attestation, CanonicalAttestationData, Ed25519PublicKey, Ed25519Signature, ResourceId, canonicalize_attestation_data, }; -use auths_verifier::types::{DeviceDID, IdentityDID}; +use auths_verifier::types::{CanonicalDid, DeviceDID}; use auths_verifier::verifier::Verifier; use chrono::{DateTime, Duration, Utc}; use ring::signature::{Ed25519KeyPair, KeyPair}; @@ -21,7 +21,7 @@ fn create_signed_attestation( let mut att = Attestation { version: 1, rid: ResourceId::new("test-rid"), - issuer: IdentityDID::new_unchecked(issuer_did), + issuer: CanonicalDid::new_unchecked(issuer_did), subject: DeviceDID::new_unchecked(subject_did), device_public_key: Ed25519PublicKey::from_bytes(device_pk), identity_signature: Ed25519Signature::empty(), diff --git a/crates/auths-verifier/tests/cases/kel_verification.rs b/crates/auths-verifier/tests/cases/kel_verification.rs index 2fd6e5c2..cc343d06 100644 --- a/crates/auths-verifier/tests/cases/kel_verification.rs +++ b/crates/auths-verifier/tests/cases/kel_verification.rs @@ -160,7 +160,7 @@ fn minimal_attestation(issuer: &str, subject: &str) -> auths_verifier::core::Att auths_verifier::core::Attestation { version: 1, rid: auths_verifier::ResourceId::new(""), - issuer: auths_verifier::IdentityDID::new_unchecked(issuer.to_string()), + issuer: auths_verifier::CanonicalDid::new_unchecked(issuer.to_string()), subject: auths_verifier::DeviceDID::new_unchecked(subject), device_public_key: auths_verifier::Ed25519PublicKey::from_bytes([0u8; 32]), identity_signature: auths_verifier::core::Ed25519Signature::empty(), diff --git a/crates/auths-verifier/tests/cases/proptest_core.rs b/crates/auths-verifier/tests/cases/proptest_core.rs index bea0d765..c47edfe4 100644 --- a/crates/auths-verifier/tests/cases/proptest_core.rs +++ b/crates/auths-verifier/tests/cases/proptest_core.rs @@ -1,7 +1,7 @@ use auths_verifier::core::{ Attestation, Capability, Ed25519PublicKey, Ed25519Signature, ResourceId, Role, ThresholdPolicy, }; -use auths_verifier::types::{DeviceDID, IdentityDID}; +use auths_verifier::types::{CanonicalDid, DeviceDID}; use chrono::{DateTime, TimeZone, Utc}; use proptest::prelude::*; @@ -21,14 +21,18 @@ fn arb_did() -> impl Strategy { }) } -/// Generate arbitrary IdentityDID -fn arb_identity_did() -> impl Strategy { - arb_did().prop_map(IdentityDID::new_unchecked) +/// Generate arbitrary CanonicalDid (must use did:keri: prefix for valid deserialization) +fn arb_canonical_did() -> impl Strategy { + proptest::string::string_regex("[A-Z][a-zA-Z0-9]{31,63}") + .unwrap() + .prop_map(|suffix| CanonicalDid::new_unchecked(format!("did:keri:{}", suffix))) } -/// Generate arbitrary DeviceDID +/// Generate arbitrary DeviceDID (must use did:key:z prefix for valid deserialization) fn arb_device_did() -> impl Strategy { - arb_did().prop_map(DeviceDID::new_unchecked) + proptest::string::string_regex("[a-zA-Z0-9]{32,64}") + .unwrap() + .prop_map(|suffix| DeviceDID::new_unchecked(format!("did:key:z{}", suffix))) } /// Generate arbitrary 32-byte public key @@ -105,7 +109,7 @@ fn arb_attestation() -> impl Strategy { // Split into two tuples to stay under 12-element limit let core_fields = ( arb_rid(), // rid - arb_identity_did(), // issuer + arb_canonical_did(), // issuer arb_device_did(), // subject arb_public_key(), // device_public_key arb_signature(), // identity_signature @@ -119,7 +123,7 @@ fn arb_attestation() -> impl Strategy { arb_optional_note(), // note arb_optional_role(), // role proptest::collection::vec(arb_capability(), 0..4), // capabilities - proptest::option::of(arb_identity_did()), // delegated_by + proptest::option::of(arb_canonical_did()), // delegated_by ); (core_fields, optional_fields).prop_map( diff --git a/crates/auths-verifier/tests/cases/revocation_adversarial.rs b/crates/auths-verifier/tests/cases/revocation_adversarial.rs index 1e4eb4f3..1aafdb20 100644 --- a/crates/auths-verifier/tests/cases/revocation_adversarial.rs +++ b/crates/auths-verifier/tests/cases/revocation_adversarial.rs @@ -3,7 +3,7 @@ use auths_verifier::core::{ Attestation, CanonicalAttestationData, Ed25519PublicKey, Ed25519Signature, ResourceId, canonicalize_attestation_data, }; -use auths_verifier::types::{DeviceDID, IdentityDID}; +use auths_verifier::types::{CanonicalDid, DeviceDID}; use auths_verifier::verify::verify_with_keys; use chrono::{DateTime, Duration, Utc}; use ring::signature::{Ed25519KeyPair, KeyPair}; @@ -24,7 +24,7 @@ fn create_signed_attestation( let mut att = Attestation { version: 1, rid: ResourceId::new("test-rid"), - issuer: IdentityDID::new_unchecked(issuer_did), + issuer: CanonicalDid::new_unchecked(issuer_did), subject: DeviceDID::new_unchecked(subject_did), device_public_key: Ed25519PublicKey::from_bytes(device_pk), identity_signature: Ed25519Signature::empty(), diff --git a/crates/auths-verifier/tests/cases/serialization_pinning.rs b/crates/auths-verifier/tests/cases/serialization_pinning.rs index 533395f7..60052915 100644 --- a/crates/auths-verifier/tests/cases/serialization_pinning.rs +++ b/crates/auths-verifier/tests/cases/serialization_pinning.rs @@ -183,12 +183,12 @@ fn environment_claim_excluded_from_canonical_form() { Attestation, CanonicalAttestationData, Ed25519PublicKey, Ed25519Signature, ResourceId, canonicalize_attestation_data, }; - use auths_verifier::types::{DeviceDID, IdentityDID}; + use auths_verifier::types::{CanonicalDid, DeviceDID}; let att = Attestation { version: 1, rid: ResourceId::new("test-rid"), - issuer: IdentityDID::new_unchecked("did:keri:ETest"), + 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(), @@ -257,12 +257,12 @@ fn environment_claim_excluded_from_canonical_form() { #[test] fn environment_claim_roundtrips_through_json() { use auths_verifier::core::{Attestation, Ed25519PublicKey, Ed25519Signature, ResourceId}; - use auths_verifier::types::{DeviceDID, IdentityDID}; + use auths_verifier::types::{CanonicalDid, DeviceDID}; let att = Attestation { version: 1, rid: ResourceId::new("test-rid"), - issuer: IdentityDID::new_unchecked("did:keri:ETest"), + 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/docs/technical/did.md b/docs/technical/did.md new file mode 100644 index 00000000..e69de29b diff --git a/packages/auths-node/src/artifact.rs b/packages/auths-node/src/artifact.rs index 45de3db1..784073b7 100644 --- a/packages/auths-node/src/artifact.rs +++ b/packages/auths-node/src/artifact.rs @@ -158,7 +158,7 @@ fn build_context_and_sign( Ok(NapiArtifactResult { attestation_json: result.attestation_json, - rid: result.rid, + rid: result.rid.to_string(), digest: result.digest, file_size, }) diff --git a/packages/auths-node/src/device.rs b/packages/auths-node/src/device.rs index 46ee3820..af58bab1 100644 --- a/packages/auths-node/src/device.rs +++ b/packages/auths-node/src/device.rs @@ -12,6 +12,7 @@ use auths_storage::git::{ }; use auths_verifier::clock::SystemClock; use auths_verifier::core::Capability; +use auths_verifier::types::DeviceDID; use napi_derive::napi; use crate::error::format_error; @@ -177,7 +178,7 @@ pub fn extend_device_authorization( let ext_config = DeviceExtensionConfig { repo_path: repo, - device_did: device_did.clone(), + device_did: DeviceDID::new_unchecked(&device_did), days, identity_key_alias: alias, device_key_alias: None, diff --git a/packages/auths-node/src/org.rs b/packages/auths-node/src/org.rs index 118fb31e..34dd54c2 100644 --- a/packages/auths-node/src/org.rs +++ b/packages/auths-node/src/org.rs @@ -16,6 +16,7 @@ use auths_sdk::workflows::org::{ }; use auths_storage::git::{GitRegistryBackend, RegistryConfig}; use auths_verifier::Capability; +use auths_verifier::PublicKeyHex; use auths_verifier::core::{Ed25519PublicKey, Role}; use auths_verifier::types::DeviceDID; use napi_derive::napi; @@ -218,13 +219,13 @@ pub fn add_org_member( )); let resolver = RegistryDidResolver::new(backend.clone()); - let admin_pk_hex = hex::encode( + let admin_pk_hex = PublicKeyHex::new_unchecked(hex::encode( resolver .resolve(&org_did) .map_err(|e| format_error("AUTHS_ORG_ERROR", e))? .public_key() .as_bytes(), - ); + )); let member_pk = if let Some(pk_hex) = member_public_key_hex { let pk_bytes = hex::decode(&pk_hex).map_err(|e| { @@ -304,13 +305,13 @@ pub fn revoke_org_member( )); let resolver = RegistryDidResolver::new(backend.clone()); - let admin_pk_hex = hex::encode( + let admin_pk_hex = PublicKeyHex::new_unchecked(hex::encode( resolver .resolve(&org_did) .map_err(|e| format_error("AUTHS_ORG_ERROR", e))? .public_key() .as_bytes(), - ); + )); let member_pk = if let Some(pk_hex) = member_public_key_hex { let pk_bytes = hex::decode(&pk_hex).map_err(|e| { diff --git a/packages/auths-node/src/trust.rs b/packages/auths-node/src/trust.rs index 0711c4bc..ea8b85c7 100644 --- a/packages/auths-node/src/trust.rs +++ b/packages/auths-node/src/trust.rs @@ -2,6 +2,7 @@ use std::path::PathBuf; use auths_core::trust::pinned::{PinnedIdentity, PinnedIdentityStore, TrustLevel}; use auths_id::identity::resolve::{DefaultDidResolver, DidResolver}; +use auths_verifier::PublicKeyHex; use napi_derive::napi; use crate::error::format_error; @@ -61,8 +62,8 @@ pub fn pin_identity( let resolver = DefaultDidResolver::with_repo(&repo); let public_key_hex = match resolver.resolve(&did) { - Ok(resolved) => hex::encode(resolved.public_key().as_bytes()), - Err(_) => String::new(), + Ok(resolved) => PublicKeyHex::new_unchecked(hex::encode(resolved.public_key().as_bytes())), + Err(_) => PublicKeyHex::new_unchecked(""), }; #[allow(clippy::disallowed_methods)] @@ -72,7 +73,7 @@ pub fn pin_identity( let _ = store.remove(&did); let pin = PinnedIdentity { did: did.clone(), - public_key_hex: if public_key_hex.is_empty() { + public_key_hex: if public_key_hex.as_ref().is_empty() { existing.public_key_hex } else { public_key_hex diff --git a/packages/auths-python/Cargo.lock b/packages/auths-python/Cargo.lock index 46d5e2d7..1466f22d 100644 --- a/packages/auths-python/Cargo.lock +++ b/packages/auths-python/Cargo.lock @@ -270,6 +270,7 @@ dependencies = [ name = "auths-policy" version = "0.0.1-rc.8" dependencies = [ + "auths-verifier", "blake3", "chrono", "serde", diff --git a/packages/auths-python/src/artifact_sign.rs b/packages/auths-python/src/artifact_sign.rs index a1f71135..f87560ff 100644 --- a/packages/auths-python/src/artifact_sign.rs +++ b/packages/auths-python/src/artifact_sign.rs @@ -187,7 +187,7 @@ fn build_context_and_sign( Ok(PyArtifactResult { attestation_json: result.attestation_json, - rid: result.rid, + rid: result.rid.to_string(), digest: result.digest, file_size, }) diff --git a/packages/auths-python/src/device_ext.rs b/packages/auths-python/src/device_ext.rs index 3e9a6386..bf57bef0 100644 --- a/packages/auths-python/src/device_ext.rs +++ b/packages/auths-python/src/device_ext.rs @@ -10,6 +10,7 @@ use auths_storage::git::{ GitRegistryBackend, RegistryAttestationStorage, RegistryConfig, RegistryIdentityStorage, }; use auths_verifier::clock::SystemClock; +use auths_verifier::types::DeviceDID; use pyo3::exceptions::{PyRuntimeError, PyValueError}; use pyo3::prelude::*; @@ -91,7 +92,7 @@ pub fn extend_device_authorization_ffi( let ext_config = DeviceExtensionConfig { repo_path: repo, - device_did: device_did.to_string(), + device_did: DeviceDID::new_unchecked(device_did), days, identity_key_alias: alias, device_key_alias: None, diff --git a/packages/auths-python/src/org.rs b/packages/auths-python/src/org.rs index 56c6b80f..57f757a1 100644 --- a/packages/auths-python/src/org.rs +++ b/packages/auths-python/src/org.rs @@ -17,9 +17,9 @@ use auths_sdk::workflows::org::{ revoke_organization_member, }; use auths_storage::git::{GitRegistryBackend, RegistryConfig}; -use auths_verifier::Capability; use auths_verifier::core::{Ed25519PublicKey, Role}; use auths_verifier::types::DeviceDID; +use auths_verifier::{Capability, PublicKeyHex}; use chrono::Utc; use crate::identity::{make_keychain_config, resolve_passphrase}; @@ -205,13 +205,13 @@ pub fn add_org_member( )); let resolver = RegistryDidResolver::new(backend.clone()); - let admin_pk_hex = hex::encode( + let admin_pk_hex = PublicKeyHex::new_unchecked(hex::encode( resolver .resolve(&org_did) .map_err(|e| PyRuntimeError::new_err(format!("[AUTHS_ORG_ERROR] {e}")))? .public_key() .as_bytes(), - ); + )); let member_pk = if let Some(pk_hex) = member_public_key_hex { let pk_bytes = hex::decode(&pk_hex).map_err(|e| { @@ -299,13 +299,13 @@ pub fn revoke_org_member( )); let resolver = RegistryDidResolver::new(backend.clone()); - let admin_pk_hex = hex::encode( + let admin_pk_hex = PublicKeyHex::new_unchecked(hex::encode( resolver .resolve(&org_did) .map_err(|e| PyRuntimeError::new_err(format!("[AUTHS_ORG_ERROR] {e}")))? .public_key() .as_bytes(), - ); + )); let member_pk = if let Some(pk_hex) = member_public_key_hex { let pk_bytes = hex::decode(&pk_hex).map_err(|e| { diff --git a/packages/auths-python/src/trust.rs b/packages/auths-python/src/trust.rs index b851a33f..ea3fd170 100644 --- a/packages/auths-python/src/trust.rs +++ b/packages/auths-python/src/trust.rs @@ -4,6 +4,7 @@ use std::path::PathBuf; use auths_core::trust::pinned::{PinnedIdentity, PinnedIdentityStore, TrustLevel}; use auths_id::identity::resolve::{DefaultDidResolver, DidResolver}; +use auths_verifier::PublicKeyHex; use chrono::Utc; fn resolve_repo(repo_path: &str) -> PathBuf { @@ -54,11 +55,13 @@ pub fn pin_identity( let resolver = DefaultDidResolver::with_repo(&repo_path); let public_key_hex = match resolver.resolve(&did) { - Ok(resolved) => hex::encode(resolved.public_key().as_bytes()), + 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 - String::new() + PublicKeyHex::new_unchecked("") } }; @@ -69,7 +72,7 @@ pub fn pin_identity( let now = Utc::now(); let pin = PinnedIdentity { did: did.clone(), - public_key_hex: if public_key_hex.is_empty() { + public_key_hex: if public_key_hex.as_ref().is_empty() { existing.public_key_hex } else { public_key_hex